fd02009741fbc7bc520000edb0c19afb6c27f29e..7c96576938ad64e30db51c597c6be3369dafe4b1
3 天以前 zhou zhou
Merge remote-tracking branch 'origin/devlop-phyz' into devlop-phyz
7c9657 对比 | 目录
3 天以前 zhou zhou
#tabsBar
01ea11 对比 | 目录
3 天以前 1
空板入库 初始
3094ce 对比 | 目录
1个文件已添加
5个文件已修改
561 ■■■■■ 已修改文件
rsf-admin/src/layout/TabsBar.jsx 364 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/layout/index.jsx 59 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/InBoundController.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/InBoundService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/InBoundServiceImpl.java 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/layout/TabsBar.jsx
New file
@@ -0,0 +1,364 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useTranslate, usePermissions } from 'react-admin';
import { Box, Tab, Tabs, IconButton, Tooltip, Divider } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import HomeIcon from '@mui/icons-material/Home';
import SettingsIcon from '@mui/icons-material/Settings';
import ClearAllIcon from '@mui/icons-material/ClearAll';
// 固定的标签页配置 (不可关闭)
const FIXED_TABS = [
    { path: '/dashboard', name: 'menu.dashboard', closable: false }
];
// 特殊页面配置
const SPECIAL_PAGES = {
    '/settings': { name: 'menu.settings', closable: true },
    '/dashboard': { name: 'menu.dashboard', closable: false }
};
// 忽略的路径模式 (登录页、根路径等)
const IGNORED_PATHS = ['/', '/login'];
// 从localStorage获取已保存的标签页
const getSavedTabs = () => {
    try {
        const saved = localStorage.getItem('openTabs');
        if (saved) {
            const parsed = JSON.parse(saved);
            // 确保 dashboard 始终存在且是第一个
            const hasDashboard = parsed.some(tab => tab.path === '/dashboard');
            if (!hasDashboard) {
                return [...FIXED_TABS, ...parsed.filter(tab => tab.path !== '/dashboard')];
            }
            // 确保 dashboard 在第一位
            const dashboard = parsed.find(tab => tab.path === '/dashboard');
            const others = parsed.filter(tab => tab.path !== '/dashboard');
            return [{ ...dashboard, closable: false }, ...others];
        }
        return [...FIXED_TABS];
    } catch {
        return [...FIXED_TABS];
    }
};
// 保存标签页到localStorage
const saveTabs = (tabs) => {
    try {
        localStorage.setItem('openTabs', JSON.stringify(tabs));
    } catch (e) {
        console.error('Failed to save tabs to localStorage:', e);
    }
};
// 规范化路径 - 处理带ID的详情/编辑页面
const normalizePath = (path) => {
    if (!path) return path;
    // 匹配模式: /resource/123/show 或 /resource/123/edit 或 /resource/123
    // 将其规范化为基础路径 /resource
    const patterns = [
        /^(\/[^/]+)\/\d+\/(show|edit)$/,  // /task/123/show -> /task
        /^(\/[^/]+)\/\d+$/,                 // /task/123 -> /task
        /^(\/[^/]+)\/create$/,              // /task/create -> /task
    ];
    for (const pattern of patterns) {
        const match = path.match(pattern);
        if (match) {
            return match[1];
        }
    }
    return path;
};
// 判断两个路径是否属于同一资源
const isSameResource = (path1, path2) => {
    return normalizePath(path1) === normalizePath(path2);
};
const TabsBar = () => {
    const location = useLocation();
    const navigate = useNavigate();
    const translate = useTranslate();
    const { permissions } = usePermissions();
    const [tabs, setTabs] = useState(getSavedTabs);
    const pendingNavigationRef = useRef(null);
    // 处理待执行的导航
    useEffect(() => {
        if (pendingNavigationRef.current) {
            navigate(pendingNavigationRef.current);
            pendingNavigationRef.current = null;
        }
    });
    // 根据路径查找菜单名称
    const findMenuName = useCallback((path, menus) => {
        if (!menus || !path) return null;
        const cleanPath = path.replace(/^\//, '');
        const normalizedCleanPath = normalizePath('/' + cleanPath).replace(/^\//, '');
        const searchMenu = (items) => {
            for (const item of items) {
                if (item.component === cleanPath || item.component === normalizedCleanPath) {
                    return item.name;
                }
                if (item.children) {
                    const found = searchMenu(item.children);
                    if (found) return found;
                }
            }
            return null;
        };
        return searchMenu(menus);
    }, []);
    // 获取标签页显示名称
    const getTabLabel = useCallback((tab) => {
        // 固定标签页直接返回翻译后的名称
        if (tab.name) {
            return translate(tab.name);
        }
        // 检查特殊页面
        const normalizedPath = normalizePath(tab.path);
        if (SPECIAL_PAGES[normalizedPath]) {
            return translate(SPECIAL_PAGES[normalizedPath].name);
        }
        // 动态标签页尝试从权限中获取名称
        if (permissions) {
            const menuName = findMenuName(tab.path, permissions);
            if (menuName) {
                return translate(menuName);
            }
        }
        // 默认返回路径
        return tab.path.replace(/^\//, '').split('/')[0] || tab.path;
    }, [translate, permissions, findMenuName]);
    // 监听路由变化,添加新标签页
    useEffect(() => {
        const currentPath = location.pathname;
        // 忽略特定路径
        if (IGNORED_PATHS.includes(currentPath)) {
            return;
        }
        setTabs(prevTabs => {
            // 检查是否已有相同资源的标签页
            const existingTabIndex = prevTabs.findIndex(tab =>
                tab.path === currentPath || isSameResource(tab.path, currentPath)
            );
            if (existingTabIndex >= 0) {
                // 如果路径完全相同,不需要更新
                if (prevTabs[existingTabIndex].path === currentPath) {
                    return prevTabs;
                }
                // 更新现有标签页的路径(保持标签位置)
                const updatedTabs = [...prevTabs];
                updatedTabs[existingTabIndex] = {
                    ...updatedTabs[existingTabIndex],
                    path: currentPath
                };
                saveTabs(updatedTabs);
                return updatedTabs;
            }
            // 查找菜单名称
            let menuName = null;
            if (permissions) {
                menuName = findMenuName(currentPath, permissions);
            }
            // 检查特殊页面
            const normalizedPath = normalizePath(currentPath);
            if (SPECIAL_PAGES[normalizedPath] && !menuName) {
                menuName = SPECIAL_PAGES[normalizedPath].name;
            }
            // 添加新标签页
            const newTab = {
                path: currentPath,
                name: menuName,
                closable: SPECIAL_PAGES[normalizedPath]?.closable !== false
            };
            const newTabs = [...prevTabs, newTab];
            saveTabs(newTabs);
            return newTabs;
        });
    }, [location.pathname, permissions, findMenuName]);
    // 切换标签页
    const handleTabChange = (event, newValue) => {
        const targetTab = tabs[newValue];
        if (targetTab && targetTab.path !== location.pathname) {
            navigate(targetTab.path);
        }
    };
    // 关闭标签页
    const handleCloseTab = (event, tabPath) => {
        event.stopPropagation();
        event.preventDefault();
        const tabIndex = tabs.findIndex(tab => tab.path === tabPath);
        const newTabs = tabs.filter(tab => tab.path !== tabPath);
        // 如果关闭的是当前标签页,需要导航到其他标签页
        if (location.pathname === tabPath || isSameResource(location.pathname, tabPath)) {
            // 优先导航到左边的标签页,否则导航到右边的
            const newIndex = Math.min(tabIndex, newTabs.length - 1);
            if (newIndex >= 0 && newTabs[newIndex]) {
                pendingNavigationRef.current = newTabs[newIndex].path;
            } else {
                pendingNavigationRef.current = '/dashboard';
            }
        }
        saveTabs(newTabs);
        setTabs(newTabs);
    };
    // 关闭所有标签页(除了dashboard)
    const handleCloseAll = () => {
        const dashboardTab = tabs.find(tab => tab.path === '/dashboard');
        const newTabs = [dashboardTab || { ...FIXED_TABS[0] }];
        saveTabs(newTabs);
        setTabs(newTabs);
        if (location.pathname !== '/dashboard') {
            navigate('/dashboard');
        }
    };
    // 获取当前标签页索引
    const currentTabIndex = tabs.findIndex(tab =>
        tab.path === location.pathname || isSameResource(tab.path, location.pathname)
    );
    const activeIndex = currentTabIndex >= 0 ? currentTabIndex : 0;
    // 判断是否有可关闭的标签
    const hasClosableTabs = tabs.some(tab => tab.closable);
    return (
        <Box
            sx={{
                width: '100%',
                backgroundColor: '#fff',
                borderBottom: '1px solid #e0e0e0',
                boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)',
                display: 'flex',
                alignItems: 'center',
            }}
        >
            <Tabs
                value={activeIndex}
                onChange={handleTabChange}
                variant="scrollable"
                scrollButtons="auto"
                sx={{
                    flex: 1,
                    minHeight: 36,
                    '& .MuiTabs-indicator': {
                        backgroundColor: '#1976d2',
                        height: 2,
                    },
                    '& .MuiTabs-scrollButtons': {
                        width: 28,
                    },
                }}
            >
                {tabs.map((tab, index) => (
                    <Tab
                        key={tab.path}
                        label={
                            <Box
                                sx={{
                                    display: 'flex',
                                    alignItems: 'center',
                                    gap: 0.5,
                                }}
                            >
                                {tab.path === '/dashboard' && (
                                    <HomeIcon sx={{ fontSize: 16 }} />
                                )}
                                {normalizePath(tab.path) === '/settings' && (
                                    <SettingsIcon sx={{ fontSize: 16 }} />
                                )}
                                <span>{getTabLabel(tab)}</span>
                                {tab.closable && (
                                    <Tooltip title="关闭">
                                        <IconButton
                                            size="small"
                                            onClick={(e) => handleCloseTab(e, tab.path)}
                                            sx={{
                                                p: 0.25,
                                                ml: 0.5,
                                                '&:hover': {
                                                    backgroundColor: 'rgba(0, 0, 0, 0.1)',
                                                },
                                            }}
                                        >
                                            <CloseIcon sx={{ fontSize: 14 }} />
                                        </IconButton>
                                    </Tooltip>
                                )}
                            </Box>
                        }
                        sx={{
                            minHeight: 36,
                            py: 0.5,
                            px: 1.5,
                            textTransform: 'none',
                            fontSize: '0.8125rem',
                            fontWeight: activeIndex === index ? 600 : 400,
                            color: activeIndex === index ? '#1976d2' : 'text.secondary',
                            backgroundColor: activeIndex === index ? 'rgba(25, 118, 210, 0.08)' : 'transparent',
                            borderRadius: '4px 4px 0 0',
                            minWidth: 'auto',
                            '&:hover': {
                                backgroundColor: 'rgba(25, 118, 210, 0.12)',
                            },
                            '&.Mui-selected': {
                                color: '#1976d2',
                            },
                        }}
                    />
                ))}
            </Tabs>
            {/* 清除所有标签按钮 */}
            {hasClosableTabs && (
                <Box sx={{ display: 'flex', alignItems: 'center', pr: 1 }}>
                    <Divider orientation="vertical" flexItem sx={{ mx: 0.5, height: 20, alignSelf: 'center' }} />
                    <Tooltip title="关闭所有标签">
                        <IconButton
                            size="small"
                            onClick={handleCloseAll}
                            sx={{
                                p: 0.5,
                                color: 'text.secondary',
                                '&:hover': {
                                    backgroundColor: 'rgba(0, 0, 0, 0.08)',
                                    color: '#d32f2f',
                                },
                            }}
                        >
                            <ClearAllIcon sx={{ fontSize: 18 }} />
                        </IconButton>
                    </Tooltip>
                </Box>
            )}
        </Box>
    );
};
export default TabsBar;
rsf-admin/src/layout/index.jsx
@@ -1,14 +1,53 @@
import { Layout as RALayout, CheckForApplicationUpdate } from "react-admin";
import { Layout as RALayout, CheckForApplicationUpdate, useSidebarState } from "react-admin";
import AppBar from './AppBar';
import { MyMenu } from './MyMenu'
import { MyMenu } from './MyMenu';
import TabsBar from './TabsBar';
import { Box } from '@mui/material';
const LayoutContent = ({ children }) => {
  const [sidebarIsOpen] = useSidebarState();
  const sidebarWidth = sidebarIsOpen ? 200 : 50;
  return (
    <RALayout
      appBar={AppBar}
      menu={MyMenu}
      sx={{
        '& .RaLayout-content': {
          position: 'absolute',
          left: `${sidebarWidth}px`,
          overflowY: 'auto',
          width: `calc(100% - ${sidebarWidth}px)`,
          height: 'calc(100% - 86px)', // 减去TabsBar的高度 (50px AppBar + 36px TabsBar)
          top: '86px',
          transition: (theme) =>
            theme.transitions.create(['left', 'width'], {
              easing: theme.transitions.easing.sharp,
              duration: theme.transitions.duration.leavingScreen,
            }),
        }
      }}
    >
      <Box sx={{
        position: 'fixed',
        top: 48,
        left: sidebarWidth,
        right: 0,
        zIndex: 1100,
        transition: (theme) =>
          theme.transitions.create('left', {
            easing: theme.transitions.easing.sharp,
            duration: theme.transitions.duration.leavingScreen,
          }),
      }}>
        <TabsBar />
      </Box>
      {children}
      <CheckForApplicationUpdate />
    </RALayout>
  );
};
export const Layout = ({ children }) => (
  <RALayout
    appBar={AppBar}
    menu={MyMenu}
    sx={{ '& .RaLayout-content': { position: 'absolute', left: '200px', overflowY: 'auto', width: 'calc(100% - 200px)', height: 'calc(100% - 50px)' } }}
  >
    {children}
    <CheckForApplicationUpdate />
  </RALayout>
  <LayoutContent>{children}</LayoutContent>
);
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/InBoundController.java
@@ -27,8 +27,7 @@
    @PostMapping("/in/emptyContainer/warehousing")
    @ApiOperation("空容器入库")
    public R emptyContainerWarehousing(@RequestBody PdaGeneralParam param) {
        return R.ok();
        return inBoundService.generateTasks(param,getLoginUserId());
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/InBoundService.java
@@ -1,10 +1,13 @@
package com.vincent.rsf.server.api.service;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.server.api.entity.params.PdaGeneralParam;
/**
 * PDA入库操作Service接口
 */
public interface InBoundService {
    R generateTasks(PdaGeneralParam param, Long loginUserId);
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/InBoundServiceImpl.java
@@ -1,8 +1,31 @@
package com.vincent.rsf.server.api.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
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.api.entity.params.PdaGeneralParam;
import com.vincent.rsf.server.api.service.InBoundService;
import com.vincent.rsf.server.api.utils.LocUtils;
import com.vincent.rsf.server.manager.controller.params.GenerateTaskParams;
import com.vincent.rsf.server.manager.entity.*;
import com.vincent.rsf.server.manager.enums.*;
import com.vincent.rsf.server.manager.service.*;
import com.vincent.rsf.server.manager.utils.LocManageUtil;
import com.vincent.rsf.server.system.constant.SerialRuleCode;
import com.vincent.rsf.server.system.utils.SerialRuleUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
 * PDA入库操作Service实现类
@@ -10,5 +33,81 @@
@Slf4j
@Service
public class InBoundServiceImpl implements InBoundService {
    @Autowired
    private DeviceSiteService deviceSiteService;
    @Autowired
    private DeviceBindService deviceBindService;
    @Autowired
    private WarehouseAreasService warehouseAreasService;
    @Autowired
    private BasContainerService basContainerService;
    @Autowired
    private BasStationService basStationService;
    @Autowired
    private LocService locService;
    @Autowired
    private TaskService taskService;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public synchronized R generateTasks(PdaGeneralParam param, Long loginUserId) {
        DeviceSite deviceSite = deviceSiteService.getOne(new LambdaQueryWrapper<DeviceSite>().eq(DeviceSite::getSite,param.getTransferStationNo()).orderByDesc(DeviceSite::getId),false);
        if (Objects.isNull(deviceSite)) {
            throw new CoolException("站点不存在!!");
        }
        DeviceBind deviceBind = deviceBindService.getById(LocUtils.getAreaType(deviceSite.getSite()));
        if (Cools.isEmpty(deviceBind)) {
            throw new CoolException("库位规则未知");
        }
        WarehouseAreas warehouseArea = warehouseAreasService.getById(deviceBind.getTypeId());
        if (Cools.isEmpty(warehouseArea)) {
            throw new CoolException("未找到所属库区信息");
        }
        BasContainer container = basContainerService.getOne(new LambdaUpdateWrapper<BasContainer>()
                .eq(BasContainer::getCode, param.getContainerNo()));
        if (Objects.isNull(container)) {
            throw new CoolException("容器未维护入库,请维护后再操作!!");
        }
        /**获取库位*/
        String targetLoc = LocManageUtil.getTargetLoc(warehouseArea.getId(), container.getContainerType());
        if (Cools.isEmpty(targetLoc)) {
            throw new CoolException("该站点对应库区未找到库位");
        }
        String ruleCode = SerialRuleUtils.generateRuleCode(SerialRuleCode.SYS_TASK_CODE, null);
        if (StringUtils.isBlank(ruleCode)) {
            throw new CoolException("编码错误:请确认编码「SYS_TASK_CODE」是否已生成!!");
        }
        Task task = new Task();
        task.setTaskCode(ruleCode)
                .setTaskStatus(TaskStsType.GENERATE_IN.id)
                .setTaskType(TaskType.TASK_TYPE_EMPITY_IN.type)
                .setWarehType(WarehType.WAREHOUSE_TYPE_AGV.val)//lsh待修改
                .setTargLoc(targetLoc)
                .setOrgSite(deviceSite.getSite())
                .setBarcode(param.getContainerNo())
                .setCreateBy(loginUserId)
                .setUpdateBy(loginUserId);
        if (!taskService.save(task)) {
            throw new CoolException("任务保存失败!!");
        }
        BasStation station = basStationService.getOne(new LambdaQueryWrapper<BasStation>()
                .eq(BasStation::getStationName, deviceSite.getSite()));
        if (Objects.isNull(station) || !station.getUseStatus().equals(LocStsType.LOC_STS_TYPE_O.type)) {
            throw new CoolException("站点不存在或站点不处于空库状态!!");
        }
        station.setUseStatus(LocStsType.LOC_STS_TYPE_R.type);
        if (!basStationService.updateById(station)) {
            throw new CoolException("站点状态更新失败!!");
        }
        if (!locService.update(new LambdaUpdateWrapper<Loc>().eq(Loc::getCode, task.getTargLoc())
                .set(Loc::getUseStatus, LocStsType.LOC_STS_TYPE_S.type).set(Loc::getBarcode, param.getContainerNo()))) {
            throw new CoolException("库位预约失败!!");
        }
        return R.ok("任务生成完毕!");
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java
@@ -468,6 +468,9 @@
                } else if (task.getTaskType().equals(TaskType.TASK_TYPE_LOC_MOVE.type)) {
                    //移库
                    moveInStock(task, loginUserId);
                } else if (task.getTaskType().equals(TaskType.TASK_TYPE_EMPITY_IN.type)) {
                    //移库
                    complateInstockE(task, loginUserId);
                }
            }
        }
@@ -1607,6 +1610,36 @@
    }
    /**
     * @author Ryan
     * @date 2025/5/20
     * @description: 完成入库任务
     * @version 1.0
     */
    @Transactional(rollbackFor = Exception.class)
    public synchronized void complateInstockE(Task task, Long loginUserId) {
        if (Objects.isNull(task)) {
            return;
        }
        Loc loc = locService.getOne(new LambdaQueryWrapper<Loc>().eq(Loc::getCode, task.getTargLoc()));
        if (Objects.isNull(loc)) {
            throw new CoolException("目标库位不存在!");
        }
        if (!loc.getUseStatus().equals(LocStsType.LOC_STS_TYPE_S.type)) {
            throw new CoolException("当前库位状态不处于S.入库预约,不可执行入库操作!");
        }
        /**修改库位状态为"D", "空板"*/
        if (!locService.update(new LambdaUpdateWrapper<Loc>().set(Loc::getUseStatus, LocStsType.LOC_STS_TYPE_D.type).eq(Loc::getCode, task.getTargLoc()))) {
            throw new CoolException("库位状态修改失败!!");
        }
        if (!this.update(new LambdaUpdateWrapper<Task>().eq(Task::getId, task.getId()).set(Task::getTaskStatus, TaskStsType.UPDATED_IN.id))) {
            throw new CoolException("任务状态修改失败!!");
        }
    }
    /**
     * @param
     * @param loginUserId
     * @return