| rsf-admin/src/layout/TabsBar.jsx | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| rsf-admin/src/layout/index.jsx | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/InBoundController.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| rsf-server/src/main/java/com/vincent/rsf/server/api/service/InBoundService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/InBoundServiceImpl.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java | ●●●●● 补丁 | 查看 | 原始文档 | 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