wanshenmean
10 天以前 035f2a81a59532ac9f892dab9ade44304847b4fb
重构输送线选择器;添加表格展开功能

C#:对 ConveyorLineTargetAddressSelector 进行了重大重构——引入了 Layer 和 TargetDeviceType 枚举,将列表替换为 HashSet/Dictionary 以实现 O(1) 的地址查找,将逻辑拆分为更小的方法(HandleConstraintMachine、HandlePinMachine、FindDevice、GetDeviceName、Get*/Set* 辅助方法),将共享逻辑整合到 ProcessDeviceRequest 中,并添加了结构化/调试日志辅助方法。提高了可读性、性能和可维护性。
Vue:在网格组件中添加了可展开行(expandable-row)支持——ViewGrid 现在传递 :tableExpand;props.js 添加了 tableExpand 属性;VolTable.vue 添加了展开列、expand 属性、expandChange 处理器、showExpand 计算属性以及展开单元格的样式。stockInfo.vue 被重写为组合式 API(Composition API),包含本地化的 TEXT 常量、详情加载逻辑、优化的列/搜索/编辑表单,并使用 tableExpand 进行行展开渲染。
杂项:.omc/project-memory.json 的时间戳/最后访问时间已更新,并清除了热路径(hotPaths);添加了一个文件名包含非 ASCII 字符的 .xlsx 文件。
已添加1个文件
已修改6个文件
1259 ■■■■ 文件已修改
.omc/project-memory.json 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLineTargetAddressSelector.cs 500 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/components/basic/ViewGrid/ViewGrid.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/components/basic/ViewGrid/props.js 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/components/basic/VolTable.vue 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/views/stock/stockInfo.vue 628 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目资料/设备协议/拘束机对接协议/拘束机对接协议.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
.omc/project-memory.json
@@ -1,6 +1,6 @@
{
  "version": "1.0.0",
  "lastScanned": 1774488038530,
  "lastScanned": 1775442733141,
  "projectRoot": "D:\\Git\\ShanMeiXinNengYuan",
  "techStack": {
    "languages": [],
@@ -36,72 +36,17 @@
      "path": "Code",
      "purpose": null,
      "fileCount": 0,
      "lastAccessed": 1774488038511,
      "lastAccessed": 1775442733121,
      "keyFiles": []
    },
    "项目资料": {
      "path": "项目资料",
      "purpose": null,
      "fileCount": 0,
      "lastAccessed": 1774488038512,
      "lastAccessed": 1775442733121,
      "keyFiles": []
    }
  },
  "hotPaths": [
    {
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_StockService\\StockSerivce.cs",
      "accessCount": 14,
      "lastAccessed": 1774492937086,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\appsettings.json",
      "accessCount": 5,
      "lastAccessed": 1774490361799,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\Program.cs",
      "accessCount": 3,
      "lastAccessed": 1774488093580,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj",
      "accessCount": 1,
      "lastAccessed": 1774488065335,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_StockService\\StockViewService.cs",
      "accessCount": 1,
      "lastAccessed": 1774492577060,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_StockService\\StockInfoService.cs",
      "accessCount": 1,
      "lastAccessed": 1774492577097,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_StockService\\StockInfoDetailService.cs",
      "accessCount": 1,
      "lastAccessed": 1774492577122,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Core\\BaseServices\\ServiceBase.cs",
      "accessCount": 1,
      "lastAccessed": 1774492577619,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_IStockService\\IStockInfoService.cs",
      "accessCount": 1,
      "lastAccessed": 1774492782997,
      "type": "file"
    }
  ],
  "hotPaths": [],
  "userDirectives": []
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLineTargetAddressSelector.cs
@@ -20,227 +20,447 @@
    public class ConveyorLineTargetAddressSelector
    {
        /// <summary>
        /// æ‹˜æŸæœºåç§°å¸¸é‡
        /// è®¾å¤‡å±‚级(上层/下层)
        /// </summary>
        private const string ConstraintMachineName = "拘束机";
        /// <remarks>
        /// ç”¨äºŽåŒºåˆ†æ‹˜æŸæœºå’Œæ’拔钉机的上层工位和下层工位。
        /// å…¥åº“任务对应上层(MaterialRequestUpper),出库任务对应下层(MaterialRequestLower)。
        /// </remarks>
        private enum Layer
        {
            /// <summary>
            /// ä¸Šå±‚工位
            /// </summary>
            Upper,
            /// <summary>
            /// ä¸‹å±‚工位
            /// </summary>
            Lower
        }
        /// <summary>
        /// æ’拔钉机名称常量
        /// ç›®æ ‡è®¾å¤‡ç±»åž‹æžšä¸¾
        /// </summary>
        private const string PinMachineName = "插拔钉机";
        /// <remarks>
        /// ç”¨äºŽæ ¹æ®ç›®æ ‡åœ°å€ç¼–码识别需要对接的设备类型。
        /// </remarks>
        private enum TargetDeviceType
        {
            /// <summary>
            /// æ— æœ‰æ•ˆè®¾å¤‡ï¼ˆåœ°å€ä¸åœ¨å·²çŸ¥èŒƒå›´å†…)
            /// </summary>
            None,
            /// <summary>
            /// æ‹˜æŸæœº
            /// </summary>
            /// <remarks>
            /// è´Ÿè´£å›ºå®š/约束电池托盘的设备,有上下两层。
            /// </remarks>
            ConstraintMachine,
            /// <summary>
            /// æ’拔钉机
            /// </summary>
            /// <remarks>
            /// è´Ÿè´£æ’针和拔针操作的设备,有上下两层。
            /// </remarks>
            PinMachine
        }
        /// <summary>
        /// æ‹˜æŸæœºå¯¹åº”的点位编码列表
        /// æ‹˜æŸæœºå¯¹åº”的点位编码集合
        /// </summary>
        /// <remarks>
        /// å½“目标地址在这些编码中时,表示需要与拘束机交互。
        /// ä½¿ç”¨ HashSet ä¿è¯ O(1) çš„ Contains æŸ¥æ‰¾æ€§èƒ½ã€‚
        /// </remarks>
        private static readonly List<string> ConstraintMachineCodes = new List<string> { "10180", "20090" };
        private static readonly HashSet<string> ConstraintMachineCodes = new HashSet<string> { "10180", "20090" };
        /// <summary>
        /// æ’拔钉机对应的点位编码列表
        /// æ’拔钉机对应的点位编码集合
        /// </summary>
        /// <remarks>
        /// å½“目标地址在这些编码中时,表示需要与插拔钉机交互。
        /// ä½¿ç”¨ HashSet ä¿è¯ O(1) çš„ Contains æŸ¥æ‰¾æ€§èƒ½ã€‚
        /// </remarks>
        private static readonly List<string> PinMachineCodes = new List<string> { "10190", "20100" };
        private static readonly HashSet<string> PinMachineCodes = new HashSet<string> { "10190", "20100" };
        /// <summary>
        /// ç›®æ ‡åœ°å€åˆ°è®¾å¤‡ç±»åž‹çš„æ˜ å°„
        /// </summary>
        /// <remarks>
        /// é€šè¿‡å•一字典实现 O(1) æŸ¥æ‰¾ï¼Œæ›¿ä»£åŽŸå…ˆåˆ†åˆ«ç”¨ä¸¤ä¸ª List + Contains + if/else if çš„写法。
        /// Key: ç›®æ ‡åœ°å€ç¼–码,Value: å¯¹åº”的设备类型。
        /// </remarks>
        private static readonly Dictionary<string, TargetDeviceType> AddressToDeviceType = new Dictionary<string, TargetDeviceType>
        {
            // æ‹˜æŸæœºçš„两个点位编码都映射到 ConstraintMachine ç±»åž‹
            { "10180", TargetDeviceType.ConstraintMachine },
            { "20090", TargetDeviceType.ConstraintMachine },
            // æ’拔钉机的两个点位编码都映射到 PinMachine ç±»åž‹
            { "10190", TargetDeviceType.PinMachine },
            { "20100", TargetDeviceType.PinMachine }
        };
        /// <summary>
        /// æ—¥å¿—记录器
        /// </summary>
        /// <remarks>
        /// é€šè¿‡ Microsoft.Extensions.Logging æŽ¥å£æ³¨å…¥ï¼Œç”¨äºŽç»“构化日志输出。
        /// </remarks>
        private readonly ILogger _logger;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="logger">日志记录器</param>
        /// <param name="logger">日志记录器,由依赖注入容器自动注入</param>
        public ConveyorLineTargetAddressSelector(ILogger logger)
        {
            _logger = logger;
            _logger = logger; // ä¿å­˜æ—¥å¿—记录器实例,供后续方法使用
        }
        /// <summary>
        /// å¤„理入库场景的下一地址请求
        /// </summary>
        /// <remarks>
        /// å½“入库任务执行到某个位置时调用此方法。
        /// åˆ¤æ–­ç›®æ ‡è®¾å¤‡æ˜¯å¦éœ€è¦ç‰©æ–™æˆ–可以出料。
        /// å…¥åº“任务到达某个位置时调用此方法,判断目标设备是否需要物料。
        /// å…¥åº“对应上层工位(Layer.Upper),因为物料从上层进入仓库。
        /// </remarks>
        /// <param name="conveyorLine">输送线设备对象</param>
        /// <param name="nextAddress">下一地址/目标设备编码</param>
        /// <param name="childDeviceCode">当前子设备编码</param>
        /// <param name="conveyorLine">输送线设备对象,用于写入目标地址和 ACK ä¿¡å·</param>
        /// <param name="nextAddress">下一地址/目标设备编码,用于识别目标设备类型</param>
        /// <param name="childDeviceCode">当前子设备编码,用于精确定位写入哪个子设备</param>
        public void HandleInboundNextAddress(CommonConveyorLine conveyorLine, string nextAddress, string childDeviceCode)
        {
            _logger.LogDebug("HandleInboundNextAddress:入库下一地址,子设备: {ChildDeviceCode},目标地址: {NextAddress}", childDeviceCode, nextAddress);
            QuartzLogger.Debug($"HandleInboundNextAddress:入库下一地址,子设备: {childDeviceCode},目标地址: {nextAddress}", conveyorLine.DeviceCode);
            // è°ƒç”¨é€šç”¨å¤„理方法,isUpper = true è¡¨ç¤ºå¤„理上层
            HandleDeviceRequest(conveyorLine, nextAddress, childDeviceCode, isUpper: true);
            // è®°å½•入库场景的调试日志,包含子设备和目标地址信息
            WriteDebug(conveyorLine, "入库下一地址", childDeviceCode, nextAddress);
            // å§”托通用处理方法,入库对应上层(isUpper: true)
            HandleDeviceRequest(conveyorLine, nextAddress, childDeviceCode, Layer.Upper);
        }
        /// <summary>
        /// å¤„理出库场景的下一地址请求
        /// </summary>
        /// <remarks>
        /// å½“出库任务执行到某个位置时调用此方法。
        /// åˆ¤æ–­ç›®æ ‡è®¾å¤‡æ˜¯å¦éœ€è¦ç‰©æ–™æˆ–可以出料。
        /// å‡ºåº“任务到达某个位置时调用此方法,判断目标设备是否需要出料。
        /// å‡ºåº“对应下层工位(Layer.Lower),因为物料从下层离开仓库。
        /// </remarks>
        /// <param name="conveyorLine">输送线设备对象</param>
        /// <param name="nextAddress">下一地址/目标设备编码</param>
        /// <param name="childDeviceCode">当前子设备编码</param>
        /// <param name="conveyorLine">输送线设备对象,用于写入目标地址和 ACK ä¿¡å·</param>
        /// <param name="nextAddress">下一地址/目标设备编码,用于识别目标设备类型</param>
        /// <param name="childDeviceCode">当前子设备编码,用于精确定位写入哪个子设备</param>
        public void HandleOutboundNextAddress(CommonConveyorLine conveyorLine, string nextAddress, string childDeviceCode)
        {
            _logger.LogDebug("HandleOutboundNextAddress:出库下一地址,子设备: {ChildDeviceCode},目标地址: {NextAddress}", childDeviceCode, nextAddress);
            QuartzLogger.Debug($"HandleOutboundNextAddress:出库下一地址,子设备: {childDeviceCode},目标地址: {nextAddress}", conveyorLine.DeviceCode);
            // è°ƒç”¨é€šç”¨å¤„理方法,isUpper = false è¡¨ç¤ºå¤„理下层
            HandleDeviceRequest(conveyorLine, nextAddress, childDeviceCode, isUpper: false);
            // è®°å½•出库场景的调试日志,包含子设备和目标地址信息
            WriteDebug(conveyorLine, "出库下一地址", childDeviceCode, nextAddress);
            // å§”托通用处理方法,出库对应下层(isUpper: false)
            HandleDeviceRequest(conveyorLine, nextAddress, childDeviceCode, Layer.Lower);
        }
        /// <summary>
        /// é€šç”¨è®¾å¤‡è¯·æ±‚处理方法
        /// æ ¹æ®ç›®æ ‡åœ°å€ç±»åž‹åˆ†å‘到对应设备处理
        /// </summary>
        /// <remarks>
        /// æ ¹æ®ç›®æ ‡åœ°å€ç±»åž‹ï¼ˆæ‹˜æŸæœº/插拔钉机)调用相应的处理逻辑。
        /// å¤„理上下层设备的物料请求和出料协调。
        /// é€šè¿‡ AddressToDeviceType å­—典将目标地址映射到设备类型,
        /// ç„¶åŽåˆ†å‘到对应的专用处理方法(HandleConstraintMachine / HandlePinMachine)。
        /// å¦‚果目标地址不在已知映射表中,直接返回,不做任何处理。
        /// </remarks>
        /// <param name="conveyorLine">输送线设备对象</param>
        /// <param name="nextAddress">下一地址/目标设备编码</param>
        /// <param name="childDeviceCode">当前子设备编码</param>
        /// <param name="isUpper">是否处理上层(true=上层,false=下层)</param>
        private void HandleDeviceRequest(CommonConveyorLine conveyorLine, string nextAddress, string childDeviceCode, bool isUpper)
        /// <param name="conveyorLine">输送线设备对象,传递到具体设备处理方法</param>
        /// <param name="nextAddress">目标设备编码,通过字典查找识别设备类型</param>
        /// <param name="childDeviceCode">子设备编码,传递到具体设备处理方法</param>
        /// <param name="layer">设备层级(上层或下层),决定读取哪组请求标志</param>
        private void HandleDeviceRequest(CommonConveyorLine conveyorLine, string nextAddress, string childDeviceCode, Layer layer)
        {
            // èŽ·å–å…¨å±€è®¾å¤‡åˆ—è¡¨
            var devices = Storage.Devices;
            // åˆ¤æ–­ç›®æ ‡è®¾å¤‡ç±»åž‹
            if (ConstraintMachineCodes.Contains(nextAddress))
            // é€šè¿‡å­—典查找目标地址对应的设备类型,如果找不到则 deviceType ä¸º None
            if (!AddressToDeviceType.TryGetValue(nextAddress, out var deviceType) || deviceType == TargetDeviceType.None)
            {
                // æ‹˜æŸæœºå¤„理分支
                // æŸ¥æ‰¾æ‹˜æŸæœºè®¾å¤‡
                ConstraintMachine? constraint = devices.OfType<ConstraintMachine>().FirstOrDefault(d => d.DeviceName == ConstraintMachineName);
                if (constraint == null)
                {
                    _logger.LogDebug("HandleDeviceRequest:未找到拘束机设备");
                    QuartzLogger.Debug("HandleDeviceRequest:未找到拘束机设备", conveyorLine.DeviceCode);
                    // æœªæ‰¾åˆ°æ‹˜æŸæœºè®¾å¤‡ï¼Œç›´æŽ¥è¿”回
                    return;
                }
                // å¤„理拘束机的请求
                ProcessDeviceRequest(
                    conveyorLine,
                    childDeviceCode,
                    // èŽ·å–ç‰©æ–™è¯·æ±‚æ ‡å¿—ï¼ˆä¸Šå±‚æˆ–ä¸‹å±‚ï¼‰
                    getMaterialRequest: () => isUpper
                        ? constraint.GetValue<ConstraintMachineDBName, short>(ConstraintMachineDBName.MaterialRequestUpper) != 0
                        : constraint.GetValue<ConstraintMachineDBName, short>(ConstraintMachineDBName.MaterialRequestLower) != 0,
                    // èŽ·å–å‡ºæ–™è¯·æ±‚æ ‡å¿—ï¼ˆä¸Šå±‚æˆ–ä¸‹å±‚ï¼‰
                    getOutputRequest: () => isUpper
                        ? constraint.GetValue<ConstraintMachineDBName, short>(ConstraintMachineDBName.OutputRequestUpper) != 0
                        : constraint.GetValue<ConstraintMachineDBName, short>(ConstraintMachineDBName.OutputRequestLower) != 0,
                    // è®¾ç½®è¾“出就绪标志(上层或下层)
                    setOutputReady: outputReq =>
                    {
                        if (isUpper)
                        {
                            constraint.SetValue(ConstraintMachineDBName.ConstraintTrayOutputReadyUpper, outputReq ? 1 : 0);
                        }
                        else
                        {
                            constraint.SetValue(ConstraintMachineDBName.ConstraintTrayOutputReadyLower, outputReq ? 1 : 0);
                        }
                    },
                    "拘束机");
                // ç›®æ ‡åœ°å€ä¸åœ¨å·²çŸ¥æ˜ å°„表中,直接返回(可能是其他类型设备)
                return;
            }
            else if (PinMachineCodes.Contains(nextAddress))
            // æ ¹æ®è¯†åˆ«å‡ºçš„设备类型分发到对应的处理方法
            switch (deviceType)
            {
                // æ’拔钉机处理分支
                // æŸ¥æ‰¾æ’拔钉机设备
                PinMachine? pinMachine = devices.OfType<PinMachine>().FirstOrDefault(d => d.DeviceName == PinMachineName);
                if (pinMachine == null)
                {
                    _logger.LogDebug("HandleDeviceRequest:未找到插拔钉机设备");
                    QuartzLogger.Debug("HandleDeviceRequest:未找到插拔钉机设备", conveyorLine.DeviceCode);
                    return;
                }
                case TargetDeviceType.ConstraintMachine:
                    // æ‹˜æŸæœºå¤„理分支:获取拘束机实例并处理其上下层请求
                    HandleConstraintMachine(conveyorLine, childDeviceCode, layer);
                    break; // å¤„理完毕,跳出 switch
                // å¤„理插拔钉机的请求
                ProcessDeviceRequest(
                    conveyorLine,
                    childDeviceCode,
                    // èŽ·å–ç‰©æ–™è¯·æ±‚æ ‡å¿—ï¼ˆä¸Šå±‚æˆ–ä¸‹å±‚ï¼‰
                    getMaterialRequest: () => isUpper
                        ? pinMachine.GetValue<PinMachineDBName, short>(PinMachineDBName.MaterialRequestUpper) != 0
                        : pinMachine.GetValue<PinMachineDBName, short>(PinMachineDBName.MaterialRequestLower) != 0,
                    // èŽ·å–å‡ºæ–™è¯·æ±‚æ ‡å¿—ï¼ˆä¸Šå±‚æˆ–ä¸‹å±‚ï¼‰
                    getOutputRequest: () => isUpper
                        ? pinMachine.GetValue<PinMachineDBName, short>(PinMachineDBName.OutputRequestUpper) != 0
                        : pinMachine.GetValue<PinMachineDBName, short>(PinMachineDBName.OutputRequestLower) != 0,
                    // è®¾ç½®è¾“出就绪标志(上层或下层)
                    setOutputReady: outputReq =>
                    {
                        if (isUpper)
                        {
                            pinMachine.SetValue(PinMachineDBName.PlugPinTrayOutputReadyUpper, outputReq ? 1 : 0);
                        }
                        else
                        {
                            pinMachine.SetValue(PinMachineDBName.PlugPinTrayOutputReadyLower, outputReq ? 1 : 0);
                        }
                    },
                    "插拔钉机");
                case TargetDeviceType.PinMachine:
                    // æ’拔钉机处理分支:获取插拔钉机实例并处理其上下层请求
                    HandlePinMachine(conveyorLine, childDeviceCode, layer);
                    break; // å¤„理完毕,跳出 switch
            }
        }
        /// <summary>
        /// å¤„理拘束机的物料/出料请求
        /// </summary>
        /// <remarks>
        /// æŸ¥æ‰¾æ‹˜æŸæœºè®¾å¤‡ï¼ŒèŽ·å–å½“å‰å±‚çº§çš„ç‰©æ–™è¯·æ±‚å’Œå‡ºæ–™è¯·æ±‚çŠ¶æ€ï¼Œ
        /// ç„¶åŽè°ƒç”¨é€šç”¨å¤„理逻辑 ProcessDeviceRequest。
        /// </remarks>
        /// <param name="conveyorLine">输送线设备对象,用于通知目标地址和 ACK</param>
        /// <param name="childDeviceCode">子设备编码,用于精确定位</param>
        /// <param name="layer">设备层级,决定读取上层还是下层的请求标志</param>
        private void HandleConstraintMachine(CommonConveyorLine conveyorLine, string childDeviceCode, Layer layer)
        {
            // ä»Žå…¨å±€è®¾å¤‡åˆ—表中查找名为"拘束机"的拘束机设备实例
            var constraint = FindDevice<ConstraintMachine>("拘束机");
            if (constraint == null)
            {
                // æœªæ‰¾åˆ°æ‹˜æŸæœºè®¾å¤‡ï¼Œå·²åœ¨ FindDevice ä¸­è®°å½•日志,此处直接返回
                return;
            }
            // èŽ·å–å½“å‰å±‚çº§ï¼ˆä¸Šå±‚/下层)的物料请求标志,非零表示设备需要物料
            bool materialRequest = GetConstraintFlag(constraint, layer, isMaterial: true);
            // èŽ·å–å½“å‰å±‚çº§ï¼ˆä¸Šå±‚/下层)的出料请求标志,非零表示设备有货要出
            bool outputRequest = GetConstraintFlag(constraint, layer, isMaterial: false);
            // æž„造设置输出就绪标志的委托(根据层级写入 Upper æˆ– Lower å¯¹åº”的寄存器)
            Action<bool> setOutputReady = outputReady =>
                SetConstraintOutputReady(constraint, layer, outputReady);
            // è°ƒç”¨é€šç”¨è¯·æ±‚处理逻辑,传入设备类型描述用于日志记录
            ProcessDeviceRequest(conveyorLine, childDeviceCode, materialRequest, outputRequest, setOutputReady, "拘束机");
        }
        /// <summary>
        /// å¤„理插拔钉机的物料/出料请求
        /// </summary>
        /// <remarks>
        /// æŸ¥æ‰¾æ’拔钉机设备,获取当前层级的物料请求和出料请求状态,
        /// ç„¶åŽè°ƒç”¨é€šç”¨å¤„理逻辑 ProcessDeviceRequest。
        /// </remarks>
        /// <param name="conveyorLine">输送线设备对象,用于通知目标地址和 ACK</param>
        /// <param name="childDeviceCode">子设备编码,用于精确定位</param>
        /// <param name="layer">设备层级,决定读取上层还是下层的请求标志</param>
        private void HandlePinMachine(CommonConveyorLine conveyorLine, string childDeviceCode, Layer layer)
        {
            // ä»Žå…¨å±€è®¾å¤‡åˆ—表中查找名为"插拔钉机"的插拔钉机设备实例
            var pinMachine = FindDevice<PinMachine>("插拔钉机");
            if (pinMachine == null)
            {
                // æœªæ‰¾åˆ°æ’拔钉机设备,已在 FindDevice ä¸­è®°å½•日志,此处直接返回
                return;
            }
            // èŽ·å–å½“å‰å±‚çº§ï¼ˆä¸Šå±‚/下层)的物料请求标志,非零表示设备需要物料
            bool materialRequest = GetPinMachineFlag(pinMachine, layer, isMaterial: true);
            // èŽ·å–å½“å‰å±‚çº§ï¼ˆä¸Šå±‚/下层)的出料请求标志,非零表示设备有货要出
            bool outputRequest = GetPinMachineFlag(pinMachine, layer, isMaterial: false);
            // æž„造设置输出就绪标志的委托(根据层级写入 Upper æˆ– Lower å¯¹åº”的寄存器)
            Action<bool> setOutputReady = outputReady =>
                SetPinMachineOutputReady(pinMachine, layer, outputReady);
            // è°ƒç”¨é€šç”¨è¯·æ±‚处理逻辑,传入设备类型描述用于日志记录
            ProcessDeviceRequest(conveyorLine, childDeviceCode, materialRequest, outputRequest, setOutputReady, "插拔钉机");
        }
        /// <summary>
        /// æŸ¥æ‰¾æŒ‡å®šåç§°çš„设备
        /// </summary>
        /// <remarks>
        /// ä»Žå…¨å±€è®¾å¤‡åˆ—表 Storage.Devices ä¸­é€šè¿‡è®¾å¤‡ç±»åž‹å’Œåç§°æŸ¥æ‰¾è®¾å¤‡å®žä¾‹ã€‚
        /// è¿™æ˜¯ä¸€ä¸ªæ³›åž‹æ–¹æ³•,可以适用于 ConstraintMachine、PinMachine ç­‰å¤šç§è®¾å¤‡ç±»åž‹ã€‚
        /// </remarks>
        /// <typeparam name="T">设备类型,必须是引用类型(如 ConstraintMachine、PinMachine)</typeparam>
        /// <param name="deviceName">设备名称,用于精确匹配设备的 DeviceName å±žæ€§</param>
        /// <returns>找到的设备实例,未找到则返回 null</returns>
        private T? FindDevice<T>(string deviceName) where T : class
        {
            // OfType<T>() ç­›é€‰å‡ºæŒ‡å®šç±»åž‹çš„设备,FirstOrDefault æŒ‰åç§°ç²¾ç¡®åŒ¹é…
            var device = Storage.Devices.OfType<T>().FirstOrDefault(d => GetDeviceName(d) == deviceName);
            if (device == null)
            {
                // è®¾å¤‡æœªæ‰¾åˆ°æ—¶è®°å½•调试日志,方便排查配置问题
                _logger.LogDebug("FindDevice:未找到 {DeviceName}", deviceName);
            }
            return device; // å¯èƒ½ä¸º null,由调用方负责 null æ£€æŸ¥
        }
        /// <summary>
        /// é€šè¿‡å¤šæ€èŽ·å–ä»»æ„è®¾å¤‡çš„åç§°
        /// </summary>
        /// <remarks>
        /// ä½¿ç”¨ switch è¡¨è¾¾å¼æ ¹æ®è®¾å¤‡çš„具体类型获取其 DeviceName å±žæ€§ï¼Œ
        /// é¿å…ä¸ºæ¯ç§è®¾å¤‡ç±»åž‹ç¼–写独立的反射或属性访问代码。
        /// å½“前支持 ConstraintMachine å’Œ PinMachine ä¸¤ç§ç±»åž‹ã€‚
        /// </remarks>
        /// <typeparam name="T">设备类型</typeparam>
        /// <param name="device">设备实例,非空</param>
        /// <returns>设备的名称字符串,如果类型不匹配则返回空字符串</returns>
        private static string GetDeviceName<T>(T device) where T : class
        {
            // æ¨¡å¼åŒ¹é…ï¼šæ ¹æ®è®¾å¤‡çš„具体运行时类型返回对应的 DeviceName
            return device switch
            {
                ConstraintMachine cm => cm.DeviceName, // æ‹˜æŸæœºè¿”回其设备名称
                PinMachine pm => pm.DeviceName,       // æ’拔钉机返回其设备名称
                _ => string.Empty                     // æœªçŸ¥ç±»åž‹è¿”回空字符串(理论上不会走到这里)
            };
        }
        /// <summary>
        /// èŽ·å–æ‹˜æŸæœºçš„è¯·æ±‚æ ‡å¿—ï¼ˆç‰©æ–™è¯·æ±‚æˆ–å‡ºæ–™è¯·æ±‚ï¼‰
        /// </summary>
        /// <remarks>
        /// æ ¹æ® isMaterial å‚数决定读取物料请求还是出料请求寄存器,
        /// å†æ ¹æ® layer å‚数决定读取上层(Upper)还是下层(Lower)寄存器。
        /// è¿”回值表示请求是否有效(非零为有效)。
        /// </remarks>
        /// <param name="constraint">拘束机设备实例,用于读取 PLC å¯„存器值</param>
        /// <param name="layer">设备层级,决定读取 Upper è¿˜æ˜¯ Lower å¯„存器</param>
        /// <param name="isMaterial">true=读取物料请求标志,false=读取出料请求标志</param>
        /// <returns>请求标志是否有效(true=有请求,false=无请求)</returns>
        private bool GetConstraintFlag(ConstraintMachine constraint, Layer layer, bool isMaterial)
        {
            // æ ¹æ® isMaterial é€‰æ‹©å¯¹åº”的寄存器名称对(物料请求或出料请求)
            var (materialKey, outputKey) = isMaterial
                ? (ConstraintMachineDBName.MaterialRequestUpper, ConstraintMachineDBName.MaterialRequestLower)   // ç‰©æ–™è¯·æ±‚
                : (ConstraintMachineDBName.OutputRequestUpper, ConstraintMachineDBName.OutputRequestLower);        // å‡ºæ–™è¯·æ±‚
            // æ ¹æ® layer é€‰æ‹©å…·ä½“使用 Upper è¿˜æ˜¯ Lower ç‰ˆæœ¬çš„寄存器
            var key = layer == Layer.Upper ? materialKey : outputKey;
            // è¯»å–寄存器值,非零表示有请求,返回布尔值
            return constraint.GetValue<ConstraintMachineDBName, short>(key) != 0;
        }
        /// <summary>
        /// èŽ·å–æ’æ‹”é’‰æœºçš„è¯·æ±‚æ ‡å¿—ï¼ˆç‰©æ–™è¯·æ±‚æˆ–å‡ºæ–™è¯·æ±‚ï¼‰
        /// </summary>
        /// <remarks>
        /// ä¸Ž GetConstraintFlag é€»è¾‘相同,但操作对象是插拔钉机(PinMachine)。
        /// æ ¹æ® isMaterial å‚数决定读取物料请求还是出料请求寄存器,
        /// å†æ ¹æ® layer å‚数决定读取上层(Upper)还是下层(Lower)寄存器。
        /// </remarks>
        /// <param name="pinMachine">插拔钉机设备实例,用于读取 PLC å¯„存器值</param>
        /// <param name="layer">设备层级,决定读取 Upper è¿˜æ˜¯ Lower å¯„存器</param>
        /// <param name="isMaterial">true=读取物料请求标志,false=读取出料请求标志</param>
        /// <returns>请求标志是否有效(true=有请求,false=无请求)</returns>
        private bool GetPinMachineFlag(PinMachine pinMachine, Layer layer, bool isMaterial)
        {
            // æ ¹æ® isMaterial é€‰æ‹©å¯¹åº”的寄存器名称对(物料请求或出料请求)
            var (materialKey, outputKey) = isMaterial
                ? (PinMachineDBName.MaterialRequestUpper, PinMachineDBName.MaterialRequestLower)   // ç‰©æ–™è¯·æ±‚
                : (PinMachineDBName.OutputRequestUpper, PinMachineDBName.OutputRequestLower);       // å‡ºæ–™è¯·æ±‚
            // æ ¹æ® layer é€‰æ‹©å…·ä½“使用 Upper è¿˜æ˜¯ Lower ç‰ˆæœ¬çš„寄存器
            var key = layer == Layer.Upper ? materialKey : outputKey;
            // è¯»å–寄存器值,非零表示有请求,返回布尔值
            return pinMachine.GetValue<PinMachineDBName, short>(key) != 0;
        }
        /// <summary>
        /// è®¾ç½®æ‹˜æŸæœºçš„输出就绪标志
        /// </summary>
        /// <remarks>
        /// å‘拘束机的 PLC å¯„存器写入输出就绪状态,告知拘束机可以继续出料。
        /// æ ¹æ® layer å‚数决定写入上层(ConstraintTrayOutputReadyUpper)还是下层(ConstraintTrayOutputReadyLower)寄存器。
        /// </remarks>
        /// <param name="constraint">拘束机设备实例,用于写入 PLC å¯„存器</param>
        /// <param name="layer">设备层级,决定写入 Upper è¿˜æ˜¯ Lower å¯„存器</param>
        /// <param name="outputReady">输出就绪状态,true=可以出料,false=不能出料</param>
        private void SetConstraintOutputReady(ConstraintMachine constraint, Layer layer, bool outputReady)
        {
            // æ ¹æ® layer é€‰æ‹©å¯¹åº”的输出就绪寄存器(Upper æˆ– Lower)
            var key = layer == Layer.Upper
                ? ConstraintMachineDBName.ConstraintTrayOutputReadyUpper   // ä¸Šå±‚输出就绪寄存器
                : ConstraintMachineDBName.ConstraintTrayOutputReadyLower;  // ä¸‹å±‚输出就绪寄存器
            // å‘ PLC å†™å…¥å€¼ï¼ŒoutputReady ä¸º true æ—¶å†™å…¥ 1,否则写入 0
            constraint.SetValue(key, outputReady ? true : false);
        }
        /// <summary>
        /// è®¾ç½®æ’拔钉机的输出就绪标志
        /// </summary>
        /// <remarks>
        /// å‘插拔钉机的 PLC å¯„存器写入输出就绪状态,告知插拔钉机可以继续出料。
        /// æ ¹æ® layer å‚数决定写入上层(PlugPinTrayOutputReadyUpper)还是下层(PlugPinTrayOutputReadyLower)寄存器。
        /// </remarks>
        /// <param name="pinMachine">插拔钉机设备实例,用于写入 PLC å¯„存器</param>
        /// <param name="layer">设备层级,决定写入 Upper è¿˜æ˜¯ Lower å¯„存器</param>
        /// <param name="outputReady">输出就绪状态,true=可以出料,false=不能出料</param>
        private void SetPinMachineOutputReady(PinMachine pinMachine, Layer layer, bool outputReady)
        {
            // æ ¹æ® layer é€‰æ‹©å¯¹åº”的输出就绪寄存器(Upper æˆ– Lower)
            var key = layer == Layer.Upper
                ? PinMachineDBName.PlugPinTrayOutputReadyUpper   // ä¸Šå±‚输出就绪寄存器
                : PinMachineDBName.PlugPinTrayOutputReadyLower; // ä¸‹å±‚输出就绪寄存器
            // å‘ PLC å†™å…¥å€¼ï¼ŒoutputReady ä¸º true æ—¶å†™å…¥ 1,否则写入 0
            pinMachine.SetValue(key, outputReady ? true : false);
        }
        /// <summary>
        /// å¤„理设备请求的核心逻辑
        /// </summary>
        /// <remarks>
        /// æ ¹æ®ç‰©æ–™è¯·æ±‚和出料请求的状态:
        /// - å¦‚果有物料请求,设置目标地址并发送 ACK
        /// - å¦‚果有出料请求,设置设备的输出就绪标志
        /// æ ¹æ®ç‰©æ–™è¯·æ±‚和出料请求的状态决定行为:
        /// - å¦‚果设备需要物料(materialRequest=true),设置输送线的目标地址为 1(有料进来)并回复 ACK ç¡®è®¤ä¿¡å·
        /// - å¦‚果设备不需要物料(materialRequest=false),则通知设备可以继续出料(setOutputReady)
        ///
        /// è¿™æ˜¯æ‹˜æŸæœºå’Œæ’拔钉机共用的处理逻辑,设备特有的部分已在此方法外封装。
        /// </remarks>
        /// <param name="conveyorLine">输送线设备对象</param>
        /// <param name="childDeviceCode">当前子设备编码</param>
        /// <param name="getMaterialRequest">获取物料请求状态的委托</param>
        /// <param name="getOutputRequest">获取出料请求状态的委托</param>
        /// <param name="setOutputReady">设置输出就绪标志的委托</param>
        /// <param name="deviceType">设备类型描述</param>
        /// <param name="conveyorLine">输送线设备对象,用于写入目标地址(Target)和 ACK ä¿¡å·</param>
        /// <param name="childDeviceCode">子设备编码,用于精确定位写入哪个子设备</param>
        /// <param name="materialRequest">物料请求标志,true=设备当前需要补充物料</param>
        /// <param name="outputRequest">出料请求标志,true=设备当前有物料需要输出(配合 materialRequest=false æ—¶ä½¿ç”¨ï¼‰</param>
        /// <param name="setOutputReady">设置设备输出就绪标志的委托,根据层级写入 Upper æˆ– Lower å¯„存器</param>
        /// <param name="deviceType">设备类型描述字符串,用于日志输出</param>
        private void ProcessDeviceRequest(
            CommonConveyorLine conveyorLine,
            string childDeviceCode,
            Func<bool> getMaterialRequest,
            Func<bool> getOutputRequest,
            bool materialRequest,
            bool outputRequest,
            Action<bool> setOutputReady,
            string deviceType)
        {
            // èŽ·å–ç‰©æ–™è¯·æ±‚çŠ¶æ€
            bool materialReq = getMaterialRequest();
            // èŽ·å–å‡ºæ–™è¯·æ±‚çŠ¶æ€
            bool outputReq = getOutputRequest();
            // è®°å½•当前请求状态的调试日志,供排查问题使用
            _logger.LogDebug("ProcessDeviceRequest:{DeviceType},子设备: {ChildDeviceCode},物料请求: {MaterialReq},出料请求: {OutputReq}",
                deviceType, childDeviceCode, materialReq, outputReq);
            QuartzLogger.Debug($"ProcessDeviceRequest:{deviceType},子设备: {childDeviceCode},物料请求: {materialReq},出料请求: {outputReq}", conveyorLine.DeviceCode);
                deviceType, childDeviceCode, materialRequest, outputRequest);
            // åŒæ­¥å†™å…¥ Quartz æ—¥å¿—文件(双写可追溯,这里保留与原逻辑一致的行为)
            QuartzLogger.Debug($"ProcessDeviceRequest:{deviceType},子设备: {childDeviceCode},物料请求: {materialRequest},出料请求: {outputRequest}", conveyorLine.DeviceCode);
            // å¦‚果设备需要物料
            if (materialReq)
            // åˆ†æ”¯åˆ¤æ–­ï¼šè®¾å¤‡æ˜¯éœ€è¦ç‰©æ–™è¿˜æ˜¯éœ€è¦å‡ºæ–™
            if (materialRequest)
            {
                // è®¾ç½®ç›®æ ‡åœ°å€ä¸º 1(表示有料进来)
                // è®¾å¤‡éœ€è¦ç‰©æ–™ -> é€šçŸ¥è¾“送线有料过来
                // 1. è®¾ç½®ç›®æ ‡åœ°å€ä¸º 1,表示"有料进入"
                conveyorLine.SetValue(ConveyorLineDBNameNew.Target, 1, childDeviceCode);
                // å›žå¤ ACK ç¡®è®¤ä¿¡å·
                // 2. å›žå¤ ACK ç¡®è®¤ä¿¡å·ï¼Œå‘ŠçŸ¥è®¾å¤‡å·²æ”¶åˆ°è¯·æ±‚
                conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_ACK, 1, childDeviceCode);
                // 3. è®°å½•信息日志,表明已完成目标地址设置和 ACK å›žå¤
                _logger.LogInformation("ProcessDeviceRequest:{DeviceType} éœ€è¦ç‰©æ–™ï¼Œå·²è®¾ç½®ç›®æ ‡åœ°å€å’ŒACK", deviceType);
                QuartzLogger.Info($"ProcessDeviceRequest:{deviceType} éœ€è¦ç‰©æ–™ï¼Œå·²è®¾ç½®ç›®æ ‡åœ°å€å’ŒACK", conveyorLine.DeviceCode);
            }
            else
            {
                // è®¾å¤‡ä¸éœ€è¦ç‰©æ–™ï¼Œè®¾ç½®è¾“出就绪标志
                // é€šçŸ¥è®¾å¤‡å¯ä»¥ç»§ç»­å‡ºæ–™
                setOutputReady(outputReq);
                // è®¾å¤‡ä¸éœ€è¦ç‰©æ–™ -> é€šçŸ¥è®¾å¤‡å¯ä»¥ç»§ç»­å‡ºæ–™ï¼ˆæ— è®ºå½“前是否有货要出,都要通知)
                // outputRequest è¡¨ç¤ºè®¾å¤‡å½“前是否确实有货,如果没有货则 outputReady=false,设备收到后等待
                setOutputReady(outputRequest);
            }
        }
        /// <summary>
        /// å†™å…¥è°ƒè¯•日志(同时输出到两个日志系统)
        /// </summary>
        /// <remarks>
        /// ç»Ÿä¸€å…¥å£ç‚¹æ—¥å¿—格式,同时向 Microsoft.Extensions.Logging å’Œ QuartzLogger å†™å…¥ï¼Œ
        /// ä¿è¯æ—¥å¿—既能在控制台查看也能在文件中追溯。
        /// </remarks>
        /// <param name="conveyorLine">输送线设备对象,用于获取设备编码写入 QuartzLogger</param>
        /// <param name="scenario">场景描述,如"入库下一地址"或"出库下一地址"</param>
        /// <param name="childDeviceCode">子设备编码</param>
        /// <param name="nextAddress">目标设备编码</param>
        private void WriteDebug(CommonConveyorLine conveyorLine, string scenario, string childDeviceCode, string nextAddress)
        {
            // å†™å…¥ç»“构化日志(可被 Serilog ç­‰æ—¥å¿—框架捕获)
            _logger.LogDebug("Handle{Scenario}:子设备: {ChildDeviceCode},目标地址: {NextAddress}",
                scenario, childDeviceCode, nextAddress);
            // å†™å…¥ Quartz ä¸“用日志文件(供定时任务轨迹追踪)
            QuartzLogger.Debug($"Handle{scenario}:子设备: {childDeviceCode},目标地址: {nextAddress}", conveyorLine.DeviceCode);
        }
    }
}
Code/WMS/WIDESEA_WMSClient/src/components/basic/ViewGrid/ViewGrid.vue
@@ -456,6 +456,7 @@
        <vol-table
          ref="table"
          :single="single"
          :expand="tableExpand"
          :rowKey="rowKey"
          :loadTreeChildren="loadTreeTableChildren"
          @loadBefore="loadTableBefore"
Code/WMS/WIDESEA_WMSClient/src/components/basic/ViewGrid/props.js
@@ -1,55 +1,61 @@
let props = {
  columns: {//当前表的配置信息
  columns: {
    type: Array,
    default: () => {
      return [];
    }
  },
  detail: {//从表明细配置
  detail: {
    type: Object,
    default: () => {
      return {
        columns: [],//从表列
        sortName: ""//从表排序字段
        columns: [],
        sortName: ""
      };
    }
  },
  editFormFields: {//新建、编辑字段(key/value)
  editFormFields: {
    type: Object,
    default: () => {
      return {};
    }
  },
  editFormOptions: {//新建、编辑配置信息
  editFormOptions: {
    type: Array,
    default: () => {
      return [];
    }
  },
  searchFormFields: {//查询字段(key/value)
  searchFormFields: {
    type: Object,
    default: () => {
      return {};
    }
  },
  searchFormOptions: {//查询配置信息(key/value)
  searchFormOptions: {
    type: Array,
    default: () => {
      return [];
    }
  },
  table: {//表的配置信息:主键、排序等
  table: {
    type: Object,
    default: () => {
      return {};
    }
  },
  extend: {//表的扩展方法与组件都合并到此属性中
  tableExpand: {
    type: Object,
    default: () => {
      return {};
    }
  },
  extend: {
    type: Object,
    default: () => {
      return {};
    }
  }
}
};
export default props;
export default props;
Code/WMS/WIDESEA_WMSClient/src/components/basic/VolTable.vue
@@ -21,6 +21,7 @@
      @select="userSelect"
      @select-all="userSelect"
      @selection-change="selectionChange"
      @expand-change="expandChange"
      @row-dblclick="rowDbClick"
      @row-click="rowClick"
      @header-click="headerClick"
@@ -38,6 +39,24 @@
      style="width: 100%"
      :scrollbar-always-on="true"
    >
      <el-table-column
        v-if="showExpand"
        type="expand"
        :fixed="expand.fixed"
        :width="expand.width || 55"
      >
        <template #default="scope">
          <div class="expand-cell">
            <table-render
              v-if="expand.render && typeof expand.render == 'function'"
              :row="scope.row"
              :index="scope.$index"
              :column="expand"
              :render="expand.render"
            ></table-render>
          </div>
        </template>
      </el-table-column>
      <el-table-column
        v-if="ck"
        type="selection"
@@ -559,6 +578,12 @@
        return 1;
      },
    },
    expand: {
      type: Object,
      default: () => {
        return {};
      },
    },
    pagination: {
      type: Object,
      default: function () {
@@ -834,6 +859,9 @@
    this.defaultLoadPage && this.load();
  },
  computed: {
    showExpand() {
      return !!(this.expand && typeof this.expand.render === "function");
    },
    filterColumns() {
      return this.columns.filter((x, index) => {
        if (!x.field) {
@@ -844,6 +872,12 @@
    },
  },
  methods: {
    expandChange(row, expandedRows) {
      if (this.expand && typeof this.expand.onChange === "function") {
        this.expand.onChange(row, expandedRows, this);
      }
      this.$emit("expandChange", { row, expandedRows });
    },
    watchRowSelectChange(newLen, oldLen) {
      if (newLen < oldLen && this.selectRows.length) {
        this.selectRows = [];
@@ -1784,6 +1818,9 @@
  border-bottom: 1px solid;
  padding-bottom: 2px;
}
.vol-table .expand-cell {
  padding: 8px 0;
}
.vol-table .empty-tag {
  border: none;
  background: none;
Code/WMS/WIDESEA_WMSClient/src/views/stock/stockInfo.vue
@@ -1,154 +1,478 @@
<template>
    <view-grid
      ref="grid"
      :columns="columns"
      :detail="detail"
      :editFormFields="editFormFields"
      :editFormOptions="editFormOptions"
      :searchFormFields="searchFormFields"
      :searchFormOptions="searchFormOptions"
      :table="table"
      :extend="extend"
    >
    </view-grid>
  </template>
    <script>
  import extend from "@/extension/stock/stockInfo.js";
  import { ref, defineComponent } from "vue";
  export default defineComponent({
    setup() {
      const table = ref({
        key: "id",
        footer: "Foots",
        cnName: "库存信息",
        name: "stockInfo",
        url: "/StockInfo/",
        sortName: "id",
      });
      const editFormFields = ref({
        deviceCode: "",
        deviceName: "",
        deviceType: "",
        deviceStatus: "",
        deviceIp: "",
        devicePort: "",
        devicePlcType: "",
        deviceRemark: "",
      });
      const editFormOptions = ref([
       [
        {field:'palletCode',title:'托盘编号',type:'string'},
        {field:'locationCode',title:'货位编号',type:'string'},
       ]
  <view-grid
    ref="grid"
    :columns="columns"
    :detail="detail"
    :editFormFields="editFormFields"
    :editFormOptions="editFormOptions"
    :searchFormFields="searchFormFields"
    :searchFormOptions="searchFormOptions"
    :table="table"
    :tableExpand="tableExpand"
    :extend="extend"
  >
  </view-grid>
</template>
<script>
import extend from "@/extension/stock/stockInfo.js";
import {
  defineComponent,
  getCurrentInstance,
  h,
  reactive,
  ref,
  resolveComponent,
} from "vue";
const TEXT = {
  pageName: "库存信息",
  palletCode: "托盘编号",
  locationCode: "货位编号",
  warehouse: "仓库",
  creator: "创建人",
  createDate: "创建时间",
  modifier: "修改人",
  modifyDate: "修改时间",
  detailName: "库存明细",
  materielName: "物料名称",
  serialNumber: "电芯码",
  stockQuantity: "库存数量",
  status: "状态",
  inboundOrderRowNo: "通道号",
  detailLoading: "库存明细加载中...",
  detailLoadFailed: "库存明细加载失败",
  detailEmpty: "当前库存头暂无明细数据",
  expandPrefix: "托盘:",
  expandMiddle: " / ",
  expandLocation: "货位:",
};
export default defineComponent({
  setup() {
    const { proxy } = getCurrentInstance();
    const ElTable = resolveComponent("el-table");
    const ElTableColumn = resolveComponent("el-table-column");
    const table = ref({
      key: "id",
      footer: "Foots",
      cnName: TEXT.pageName,
      name: "stockInfo",
      url: "/StockInfo/",
      sortName: "id",
    });
    const editFormFields = ref({
      palletCode: "",
      locationCode: "",
    });
    const editFormOptions = ref([
      [
        { field: "palletCode", title: TEXT.palletCode, type: "string" },
        { field: "locationCode", title: TEXT.locationCode, type: "string" },
      ],
    ]);
    const searchFormFields = ref({
      palletCode: "",
      locationCode: "",
    });
    const searchFormOptions = ref([
      [
        { title: TEXT.palletCode, field: "palletCode", type: "like" },
        { title: TEXT.locationCode, field: "locationCode", type: "like" },
      ],
    ]);
    const columns = ref([
      {
        field: "id",
        title: "Id",
        type: "int",
        width: 90,
        hidden: true,
        readonly: true,
        require: true,
        align: "left",
      },
      {
        field: "palletCode",
        title: TEXT.palletCode,
        type: "string",
        width: 120,
        align: "left",
      },
      {
        field: "locationCode",
        title: TEXT.locationCode,
        type: "string",
        width: 150,
        align: "left",
      },
      {
        field: "warehouseId",
        title: TEXT.warehouse,
        type: "select",
        width: 100,
        align: "left",
        bind: { key: "warehouseEnum", data: [] },
      },
      {
        field: "creater",
        title: TEXT.creator,
        type: "string",
        width: 90,
        align: "left",
      },
      {
        field: "createDate",
        title: TEXT.createDate,
        type: "datetime",
        width: 160,
        align: "left",
      },
      {
        field: "modifier",
        title: TEXT.modifier,
        type: "string",
        width: 100,
        align: "left",
        hidden: true,
      },
      {
        field: "modifyDate",
        title: TEXT.modifyDate,
        type: "datetime",
        width: 160,
        align: "left",
        hidden: true,
      },
    ]);
    const detail = ref({
      cnName: "#detailCnName",
      table: "",
      columns: [],
      sortName: "",
    });
    const detailState = reactive({
      rowsMap: {},
      loadingMap: {},
      errorMap: {},
    });
    const stockStatusOptions = ref([]);
    const detailColumns = [
      { field: "materielName", title: TEXT.materielName, minWidth: 160 },
      { field: "serialNumber", title: TEXT.serialNumber, minWidth: 160 },
      { field: "stockQuantity", title: TEXT.stockQuantity, minWidth: 120 },
      { field: "status", title: TEXT.status, minWidth: 120 },
      { field: "inboundOrderRowNo", title: TEXT.inboundOrderRowNo, minWidth: 120 },
    ];
    const normalizeValue = (value) => {
      return value === null || value === undefined || value === "" ? "--" : value;
    };
    const formatStatusText = (value) => {
      const matched = stockStatusOptions.value.find((item) => `${item.key}` === `${value}`);
      return matched ? matched.value || matched.label : normalizeValue(value);
    };
    const getDetailRows = (stockId) => {
      return detailState.rowsMap[stockId] || [];
    };
    const loadDetailRows = async (row) => {
      if (!row || !row.id || detailState.loadingMap[row.id]) {
        return;
      }
      if (detailState.rowsMap[row.id]) {
        return;
      }
      detailState.loadingMap[row.id] = true;
      detailState.errorMap[row.id] = "";
      try {
        const result = await proxy.http.post("/api/StockInfoDetail/getPageData", {
          page: 1,
          rows: 200,
          sort: "id",
          order: "asc",
          wheres: JSON.stringify([
            {
              name: "stockId",
              value: String(row.id),
              displayType: "int",
            },
          ]),
        });
        detailState.rowsMap[row.id] = (result && result.rows) || [];
      } catch (error) {
        detailState.rowsMap[row.id] = null;
        detailState.errorMap[row.id] = error?.message || TEXT.detailLoadFailed;
      } finally {
        detailState.loadingMap[row.id] = false;
      }
    };
    const loadStockStatusOptions = async () => {
      try {
        const result = await proxy.http.post("/api/Sys_Dictionary/GetVueDictionary", ["stockStatusEmun"]);
        const matched = (result || []).find((item) => item.dicNo === "stockStatusEmun");
        stockStatusOptions.value = matched ? matched.data || [] : [];
      } catch (error) {
        stockStatusOptions.value = [];
      }
    };
    loadStockStatusOptions();
    const renderStatus = (row) => {
      if (detailState.loadingMap[row.id]) {
        return h("div", { class: "stock-detail-status" }, TEXT.detailLoading);
      }
      if (detailState.errorMap[row.id]) {
        return h(
          "div",
          { class: "stock-detail-status stock-detail-status--error" },
          detailState.errorMap[row.id]
        );
      }
      return null;
    };
    const renderDetailTable = (row) => {
      const statusNode = renderStatus(row);
      if (statusNode) {
        return statusNode;
      }
      const rows = getDetailRows(row.id);
      if (!rows.length) {
        return h("div", { class: "stock-detail-status" }, TEXT.detailEmpty);
      }
      return h("div", { class: "stock-detail-table-wrapper" }, [
        h("div", { class: "stock-detail-toolbar" }, [
          h("div", { class: "stock-detail-toolbar__left" }, TEXT.detailName),
          h("div", { class: "stock-detail-toolbar__right" }, [
            h("span", { class: "stock-detail-count" }, `${rows.length} æ¡`),
          ]),
        ]),
        h(
          ElTable,
          {
            data: rows,
            border: true,
            stripe: true,
            size: "small",
            class: "stock-detail-el-table",
            maxHeight: 420,
            emptyText: TEXT.detailEmpty,
          },
          () =>
            detailColumns.map((column) =>
              h(ElTableColumn, {
                key: column.field,
                prop: column.field,
                label: column.title,
                minWidth: column.minWidth,
                showOverflowTooltip: true,
                formatter: (detailRow) =>
                  column.field === "status"
                    ? formatStatusText(detailRow[column.field])
                    : normalizeValue(detailRow[column.field]),
              })
            )
        ),
      ]);
      const searchFormFields = ref({
        palletCode: "",
        locationCode: "",
      });
      const searchFormOptions = ref([
        [
          { title: "托盘编号", field: "palletCode" },
          { title: "货位编号", field: "locationCode" },
        ],
      ]);
      const columns = ref([
        {
          field: "id",
          title: "Id",
          type: "int",
          width: 90,
          hidden: true,
          readonly: true,
          require: true,
          align: "left",
        },
        {
          field: "palletCode",
          title: "托盘编号",
          type: "string",
          width: 90,
          align: "left",
        },
        {
          field: "locationCode",
          title: "货位编号",
          type: "string",
          width: 150,
          align: "left",
        },
        // {
        //   field: "isFull",
        //   title: "是否满盘",
        //   type: "string",
        //   width: 150,
        //   align: "left",
        //   bind: { key: "yesno", data: [] },
        // },
         {
          field: "warehouseId",
          title: "仓库",
          type: "select",
          width: 100,
          align: "left",
          bind: { key: "warehouseEnum", data: [] },
        },
        {
          field: "creater",
          title: "创建人",
          type: "string",
          width: 90,
          align: "left",
        },
        {
          field: "createDate",
          title: "创建时间",
          type: "datetime",
          width: 160,
          align: "left",
        },
        {
          field: "modifier",
          title: "修改人",
          type: "string",
          width: 100,
          align: "left",
          hidden:true
        },
        {
          field: "modifyDate",
          title: "修改时间",
          type: "datetime",
          width: 160,
          align: "left",
          hidden:true
        },
        {
          field: "remark",
          title: "备注",
          type: "string",
          width: 100,
          align: "left",
          hidden:true
        },
      ]);
      const detail = ref({
        cnName: "#detailCnName",
        table: "",
        columns: [],
        sortName: "",
      });
      return {
        table,
        extend,
        editFormFields,
        editFormOptions,
        searchFormFields,
        searchFormOptions,
        columns,
        detail,
      };
    },
  });
  </script>
    };
    const tableExpand = ref({
      width: 55,
      onChange(row, expandedRows) {
        const isExpanded = expandedRows.some((item) => item.id === row.id);
        if (isExpanded) {
          loadDetailRows(row);
        }
      },
      render(render, { row }) {
        return render("div", { class: "stock-detail-panel" }, [
          render("div", { class: "stock-detail-header" }, [
            render("div", { class: "stock-detail-header__main" }, [
              render("div", { class: "stock-detail-title" }, TEXT.detailName),
              render(
                "div",
                { class: "stock-detail-subtitle" },
                `${TEXT.expandPrefix}${normalizeValue(row.palletCode)}${TEXT.expandMiddle}${TEXT.expandLocation}${normalizeValue(row.locationCode)}`
              ),
            ]),
            // render("div", { class: "stock-detail-tags" }, [
            //   render("span", { class: "stock-detail-tag" }, normalizeValue(row.palletCode)),
            //   render(
            //     "span",
            //     { class: "stock-detail-tag stock-detail-tag--muted" },
            //     normalizeValue(row.locationCode)
            //   ),
            // ]),
          ]),
          renderDetailTable(row),
        ]);
      },
    });
    return {
      table,
      extend,
      editFormFields,
      editFormOptions,
      searchFormFields,
      searchFormOptions,
      columns,
      detail,
      tableExpand,
    };
  },
});
</script>
<style scoped>
.stock-detail-panel {
  margin: 4px 8px 12px;
  padding: 14px 16px 16px;
  background: linear-gradient(180deg, #ffffff 0%, #fafbfc 100%);
  border: 1px solid #e8edf3;
  border-radius: 10px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
}
.stock-detail-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 12px;
  padding-bottom: 12px;
  border-bottom: 1px solid #edf1f5;
}
.stock-detail-header__main {
  min-width: 0;
}
.stock-detail-title {
  margin-bottom: 4px;
  font-size: 15px;
  font-weight: 700;
  color: #303133;
}
.stock-detail-subtitle {
  font-size: 13px;
  color: #606266;
  line-height: 1.6;
}
.stock-detail-tags {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 8px;
}
.stock-detail-tag {
  display: inline-flex;
  align-items: center;
  height: 28px;
  padding: 0 10px;
  color: #1f5eff;
  background: #edf4ff;
  border: 1px solid #d8e6ff;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
}
.stock-detail-tag--muted {
  color: #4e5969;
  background: #f4f6f8;
  border-color: #e5e9ef;
}
.stock-detail-status {
  padding: 14px 12px;
  color: #606266;
  background: #f8fafc;
  border: 1px dashed #d9e2ec;
  border-radius: 8px;
}
.stock-detail-status--error {
  color: #f56c6c;
  background: #fef0f0;
  border-color: #fde2e2;
}
.stock-detail-table-wrapper {
  overflow-x: auto;
  border: 1px solid #ebeef5;
  border-radius: 8px;
  background: #fff;
}
.stock-detail-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 14px;
  background: #f8fafc;
  border-bottom: 1px solid #edf1f5;
}
.stock-detail-toolbar__left {
  font-size: 13px;
  font-weight: 600;
  color: #303133;
}
.stock-detail-count {
  display: inline-flex;
  align-items: center;
  height: 24px;
  padding: 0 10px;
  color: #606266;
  background: #fff;
  border: 1px solid #e5e9ef;
  border-radius: 999px;
  font-size: 12px;
}
:deep(.stock-detail-el-table) {
  border-top: none;
}
:deep(.stock-detail-el-table .el-table__inner-wrapper::before) {
  display: none;
}
:deep(.stock-detail-el-table th.el-table__cell) {
  background: #f5f7fa;
  color: #303133;
  font-weight: 600;
}
:deep(.stock-detail-el-table td.el-table__cell) {
  color: #606266;
}
:deep(.stock-detail-el-table .el-table__body tr:hover > td.el-table__cell) {
  background: #f0f7ff;
}
</style>
ÏîÄ¿×ÊÁÏ/É豸ЭÒé/¾ÐÊø»ú¶Ô½ÓЭÒé/¾ÐÊø»ú¶Ô½ÓЭÒé.xlsx
Binary files differ