优化仓库分类管理,新增兼容历史分类编码的支持,重构相关服务和控制器以提升系统的可维护性和扩展性。同时,增强原材料卡片和库区管理的查询逻辑,确保数据展示的准确性和一致性。
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
package org.jeecg.modules.xslmes.capacity;
|
||||
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 库区展示用「实际存放量」数据源贡献者(可多实现并按 Spring Order 编排)
|
||||
*/
|
||||
public interface WarehouseAreaActualCapacityContribution {
|
||||
|
||||
/**
|
||||
* @param areas 通常为列表/分页的一段记录(内存回填,不写库)
|
||||
*/
|
||||
void contribute(List<MesXslWarehouseArea> areas);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.jeecg.modules.xslmes.capacity;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 编排库区「实际存放量」展示回填(可多 {@link WarehouseAreaActualCapacityContribution} 扩展)。
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class WarehouseAreaDisplayedActualCapacityMediator {
|
||||
|
||||
private final List<WarehouseAreaActualCapacityContribution> contributions;
|
||||
|
||||
public void apply(Collection<MesXslWarehouseArea> areas) {
|
||||
if (areas == null || areas.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<MesXslWarehouseArea> list = new ArrayList<>(areas);
|
||||
List<WarehouseAreaActualCapacityContribution> ordered = contributions.stream().sorted(AnnotationAwareOrderComparator.INSTANCE).toList();
|
||||
for (WarehouseAreaActualCapacityContribution c : ordered) {
|
||||
c.contribute(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package org.jeecg.modules.xslmes.capacity.impl;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.modules.xslmes.capacity.WarehouseAreaActualCapacityContribution;
|
||||
import org.jeecg.modules.xslmes.config.XslMesWarehouseAreaCapacityProperties;
|
||||
import org.jeecg.modules.xslmes.dto.MesXslAreaRemainQtySumDTO;
|
||||
import org.jeecg.modules.system.service.ISysCategoryService;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
|
||||
import org.jeecg.modules.xslmes.mapper.MesXslRawMaterialCardMapper;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用「原材料卡片」按库区(trim)汇总 remaining_quantity,回填展示字段 actual_capacity(不写库)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(100)
|
||||
@RequiredArgsConstructor
|
||||
public class RawMaterialCardRemainQtyCapacityContribution implements WarehouseAreaActualCapacityContribution {
|
||||
|
||||
private final XslMesWarehouseAreaCapacityProperties properties;
|
||||
private final MesXslRawMaterialCardMapper mesXslRawMaterialCardMapper;
|
||||
private final ISysCategoryService sysCategoryService;
|
||||
|
||||
@Override
|
||||
public void contribute(List<MesXslWarehouseArea> areas) {
|
||||
if (areas == null || areas.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!properties.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
Set<String> allowedCategories = resolveAllowedWarehouseCategoryIds();
|
||||
if (allowedCategories.isEmpty()) {
|
||||
log.debug("[WarehouseAreaActualCapacity] 原材料库分类编码/ID 均未解析到有效 warehouse_category,跳过 RawMaterialCard 策略");
|
||||
return;
|
||||
}
|
||||
|
||||
List<MesXslWarehouseArea> targets =
|
||||
areas.stream().filter(a -> a != null && allowedCategories.contains(normId(a.getWarehouseCategory())) && StringUtils.isNotBlank(a.getAreaCode()))
|
||||
.collect(Collectors.toList());
|
||||
if (targets.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Collection<String> codes =
|
||||
targets.stream().map(MesXslWarehouseArea::getAreaCode).map(String::trim).filter(StringUtils::isNotBlank).distinct().collect(Collectors.toList());
|
||||
|
||||
Map<String, Long> sumMap = loadSumMap(codes);
|
||||
for (MesXslWarehouseArea a : targets) {
|
||||
String ck = trimKey(a.getAreaCode());
|
||||
long sum = sumMap.getOrDefault(ck, 0L);
|
||||
int display;
|
||||
try {
|
||||
display = Math.toIntExact(Math.min(sum, Integer.MAX_VALUE));
|
||||
} catch (ArithmeticException ex) {
|
||||
display = Integer.MAX_VALUE;
|
||||
}
|
||||
a.setActualCapacity(display);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Long> loadSumMap(Collection<String> codes) {
|
||||
if (codes.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<MesXslAreaRemainQtySumDTO> rows = mesXslRawMaterialCardMapper.sumRemainingQtyByTrimmedWarehouseArea(codes);
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, Long> map = new HashMap<>(Math.max(16, rows.size() * 2));
|
||||
for (MesXslAreaRemainQtySumDTO row : rows) {
|
||||
if (row == null || row.getAreaCode() == null) {
|
||||
continue;
|
||||
}
|
||||
String k = row.getAreaCode().trim();
|
||||
if (StringUtils.isBlank(k)) {
|
||||
continue;
|
||||
}
|
||||
long v = row.getQtySum() == null ? 0L : row.getQtySum();
|
||||
map.put(k, v);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private static String trimKey(String code) {
|
||||
return code == null ? "" : code.trim();
|
||||
}
|
||||
|
||||
private static String normId(String id) {
|
||||
return StringUtils.trimToEmpty(id);
|
||||
}
|
||||
|
||||
private static Set<String> normalizeIdSet(Collection<String> raw) {
|
||||
if (raw == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return raw.stream().filter(StringUtils::isNotBlank).map(String::trim).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/** 配置的 id + 配置的 code 解析出的 id 合并 */
|
||||
private Set<String> resolveAllowedWarehouseCategoryIds() {
|
||||
Set<String> set = new HashSet<>(normalizeIdSet(properties.getRawMaterialWarehouseCategoryIds()));
|
||||
Collection<String> codes = properties.getRawMaterialWarehouseCategoryCodes();
|
||||
if (codes != null) {
|
||||
for (String c : codes) {
|
||||
if (StringUtils.isBlank(c)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
String id = sysCategoryService.queryIdByCode(c.trim());
|
||||
if (StringUtils.isNotBlank(id)) {
|
||||
set.add(id.trim());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.warn("[WarehouseAreaActualCapacity] 解析原材料库分类编码失败 code={}: {}", c, ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.jeecg.modules.xslmes.config;
|
||||
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import org.jeecg.modules.xslmes.constant.MesXslWarehouseCategory;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
|
||||
* 库区列表「实际存放量」展示口径配置(不写库)
|
||||
|
||||
* <p>
|
||||
|
||||
* 推荐使用 {@link #rawMaterialWarehouseCategoryCodes},启动时解析为 sys_category.id,
|
||||
|
||||
* 避免在配置里维护易变的雪花 id;也可用 {@link #rawMaterialWarehouseCategoryIds} 直接指定 id。
|
||||
|
||||
*/
|
||||
|
||||
@Data
|
||||
|
||||
@Component
|
||||
|
||||
@ConfigurationProperties(prefix = "xslmes.warehouse-area.display-actual-capacity")
|
||||
|
||||
public class XslMesWarehouseAreaCapacityProperties {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
@@ -1,7 +1,19 @@
|
||||
package org.jeecg.modules.xslmes.constant;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* MES 仓库分类(分类字典 sys_category,根编码 {@link #ROOT_CODE})
|
||||
*
|
||||
* <p>说明:历史版本曾在编码上使用 F1/F2 后缀;当前业务树已不再按楼层区分,
|
||||
* 「F1」「F2」仅可能作为编码后缀保留,请以 {@link #RAW_MATERIAL_CATEGORY_CODES}、
|
||||
* 客户/供应商库编码集合为准做兼容。</p>
|
||||
*/
|
||||
public final class MesXslWarehouseCategory {
|
||||
|
||||
@@ -10,8 +22,37 @@ public final class MesXslWarehouseCategory {
|
||||
/** 根节点编码,与 JCategorySelect 的 pcode 一致 */
|
||||
public static final String ROOT_CODE = "XSLMES_WH";
|
||||
|
||||
/** 客户库(二楼):须关联客户 */
|
||||
public static final String CUSTOMER_CATEGORY_CODE = "XSLMES_WH_F2_KH";
|
||||
/** 供应商库(二楼):须关联供应商 */
|
||||
public static final String SUPPLIER_CATEGORY_CODE = "XSLMES_WH_F2_GYS";
|
||||
/** 兼容历史:原材料库在库中可能出现的分类编码(新环境多为 F1,老数据可能仍为 F2) */
|
||||
public static final List<String> RAW_MATERIAL_CATEGORY_CODES =
|
||||
Collections.unmodifiableList(Arrays.asList("XSLMES_WH_F1_YCL", "XSLMES_WH_F2_YCL"));
|
||||
|
||||
/** 兼容历史编号的「客户库」分类编码集合 */
|
||||
private static final Set<String> CUSTOMER_CATEGORY_CODES =
|
||||
Collections.unmodifiableSet(new HashSet<>(Arrays.asList("XSLMES_WH_F1_KH", "XSLMES_WH_F2_KH")));
|
||||
|
||||
/** 兼容历史编号的「供应商库」分类编码集合 */
|
||||
private static final Set<String> SUPPLIER_CATEGORY_CODES =
|
||||
Collections.unmodifiableSet(new HashSet<>(Arrays.asList("XSLMES_WH_F1_GYS", "XSLMES_WH_F2_GYS")));
|
||||
|
||||
/**
|
||||
* @deprecated 请改用 {@link #isCustomerWarehouseCategoryCode(String)}
|
||||
*/
|
||||
@Deprecated
|
||||
public static final String CUSTOMER_CATEGORY_CODE = "XSLMES_WH_F1_KH";
|
||||
|
||||
/**
|
||||
* @deprecated 请改用 {@link #isSupplierWarehouseCategoryCode(String)}
|
||||
*/
|
||||
@Deprecated
|
||||
public static final String SUPPLIER_CATEGORY_CODE = "XSLMES_WH_F1_GYS";
|
||||
|
||||
/** 是否为「客户库」分类编码(兼容 XSLMES_WH_F1_KH / XSLMES_WH_F2_KH) */
|
||||
public static boolean isCustomerWarehouseCategoryCode(String code) {
|
||||
return CUSTOMER_CATEGORY_CODES.contains(StringUtils.trimToEmpty(code));
|
||||
}
|
||||
|
||||
/** 是否为「供应商库」分类编码(兼容 XSLMES_WH_F1_GYS / XSLMES_WH_F2_GYS) */
|
||||
public static boolean isSupplierWarehouseCategoryCode(String code) {
|
||||
return SUPPLIER_CATEGORY_CODES.contains(StringUtils.trimToEmpty(code));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -702,6 +703,7 @@ public class MesXslDesktopAnonController {
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<MesXslWarehouseArea> qw = QueryGenerator.initQueryWrapper(mesXslWarehouseArea, req.getParameterMap());
|
||||
IPage<MesXslWarehouseArea> page = warehouseAreaService.page(new Page<>(pageNo, pageSize), qw);
|
||||
warehouseAreaService.enrichDisplayedActualCapacity(page.getRecords());
|
||||
return Result.OK(page);
|
||||
}
|
||||
|
||||
@@ -709,7 +711,11 @@ public class MesXslDesktopAnonController {
|
||||
@GetMapping("/xslmes/mesXslWarehouseArea/anon/queryById")
|
||||
public Result<MesXslWarehouseArea> warehouseAreaAnonQueryById(@RequestParam(name = "id") String id) {
|
||||
MesXslWarehouseArea entity = warehouseAreaService.getById(id);
|
||||
return entity != null ? Result.OK(entity) : Result.error("未找到对应数据");
|
||||
if (entity == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
warehouseAreaService.enrichDisplayedActualCapacity(Collections.singletonList(entity));
|
||||
return Result.OK(entity);
|
||||
}
|
||||
|
||||
@Operation(summary = "库区-免密添加")
|
||||
|
||||
@@ -16,6 +16,7 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.apache.shiro.authz.annotation.Logical;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
@@ -81,7 +82,36 @@ public class MesXslRawMaterialCardController extends JeecgController<MesXslRawMa
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
// 库区条件:去掉实体上的值避免 QueryGenerator 再拼一班;用 TRIM 与看板聚合一致(避免首尾空格导致「有汇总无明细」)
|
||||
String rawWarehouseArea = mesXslRawMaterialCard != null ? mesXslRawMaterialCard.getWarehouseArea() : null;
|
||||
String areaEq = oConvertUtils.isNotEmpty(rawWarehouseArea) ? rawWarehouseArea.trim() : "";
|
||||
boolean filterByWarehouseArea = !areaEq.isEmpty();
|
||||
if (filterByWarehouseArea) {
|
||||
mesXslRawMaterialCard.setWarehouseArea(null);
|
||||
}
|
||||
QueryWrapper<MesXslRawMaterialCard> queryWrapper = QueryGenerator.initQueryWrapper(mesXslRawMaterialCard, req.getParameterMap());
|
||||
if (filterByWarehouseArea) {
|
||||
queryWrapper.apply("TRIM(COALESCE(warehouse_area,'')) = {0}", areaEq);
|
||||
}
|
||||
// 看板等扩展查询:remaining_quantity「有剩余 / 无剩余」(非实体字段,单独拼条件)
|
||||
String remainQtyFilter = req.getParameter("remainQtyFilter");
|
||||
if ("has".equals(remainQtyFilter)) {
|
||||
queryWrapper.gt("remaining_quantity", 0);
|
||||
} else if ("none".equals(remainQtyFilter)) {
|
||||
queryWrapper.and(w -> w.isNull("remaining_quantity").or().le("remaining_quantity", 0));
|
||||
}
|
||||
String mixKw = req.getParameter("mixKeyword");
|
||||
if (StringUtils.isNotBlank(mixKw)) {
|
||||
final String kw = mixKw.trim();
|
||||
queryWrapper.and(
|
||||
w -> w.like("barcode", kw)
|
||||
.or()
|
||||
.like("batch_no", kw)
|
||||
.or()
|
||||
.like("material_name", kw)
|
||||
.or()
|
||||
.like("material_id", kw));
|
||||
}
|
||||
Page<MesXslRawMaterialCard> page = new Page<>(pageNo, pageSize);
|
||||
IPage<MesXslRawMaterialCard> pageList = mesXslRawMaterialCardService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.jeecg.modules.xslmes.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialWarehouseBoardService;
|
||||
import org.jeecg.modules.xslmes.vo.MesXslRawMaterialWarehouseBoardVO;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 原材料库区看板:库区主数据 + 原材料卡片聚合
|
||||
*/
|
||||
@Tag(name = "原材料库区看板")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/mesXslRawMaterialWarehouseBoard")
|
||||
@Slf4j
|
||||
public class MesXslRawMaterialWarehouseBoardController {
|
||||
|
||||
@Autowired
|
||||
private IMesXslRawMaterialWarehouseBoardService mesXslRawMaterialWarehouseBoardService;
|
||||
|
||||
@Operation(summary = "原材料库区看板-汇总数据")
|
||||
@RequiresPermissions("xslmes:mes_xsl_raw_material_warehouse_board:list")
|
||||
@GetMapping(value = "/board")
|
||||
public Result<MesXslRawMaterialWarehouseBoardVO> board(
|
||||
@RequestParam(name = "warehouseId", required = false) String warehouseId,
|
||||
@RequestParam(name = "keyword", required = false) String keyword,
|
||||
@RequestParam(name = "measureType", required = false) String measureType) {
|
||||
MesXslRawMaterialWarehouseBoardVO data = mesXslRawMaterialWarehouseBoardService.queryBoard(warehouseId, keyword, measureType);
|
||||
return Result.OK(data);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package org.jeecg.modules.xslmes.controller;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -13,15 +14,22 @@ import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslWarehouseAreaService;
|
||||
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
|
||||
import org.jeecgframework.poi.excel.def.NormalExcelConstants;
|
||||
import org.jeecgframework.poi.excel.entity.ExportParams;
|
||||
import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
|
||||
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -36,6 +44,9 @@ public class MesXslWarehouseAreaController extends JeecgController<MesXslWarehou
|
||||
@Autowired
|
||||
private IMesXslWarehouseAreaService mesXslWarehouseAreaService;
|
||||
|
||||
@Autowired
|
||||
private JeecgBaseConfig jeecgBaseConfig;
|
||||
|
||||
@Autowired
|
||||
private MesXslStompNotifyService stompNotify;
|
||||
|
||||
@@ -49,6 +60,7 @@ public class MesXslWarehouseAreaController extends JeecgController<MesXslWarehou
|
||||
QueryWrapper<MesXslWarehouseArea> queryWrapper = QueryGenerator.initQueryWrapper(mesXslWarehouseArea, req.getParameterMap());
|
||||
Page<MesXslWarehouseArea> page = new Page<>(pageNo, pageSize);
|
||||
IPage<MesXslWarehouseArea> pageList = mesXslWarehouseAreaService.page(page, queryWrapper);
|
||||
mesXslWarehouseAreaService.enrichDisplayedActualCapacity(pageList.getRecords());
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
@@ -166,13 +178,33 @@ public class MesXslWarehouseAreaController extends JeecgController<MesXslWarehou
|
||||
if (entity == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
mesXslWarehouseAreaService.enrichDisplayedActualCapacity(Collections.singletonList(entity));
|
||||
return Result.OK(entity);
|
||||
}
|
||||
|
||||
@RequiresPermissions("xslmes:mes_xsl_warehouse_area:exportXls")
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, MesXslWarehouseArea mesXslWarehouseArea) {
|
||||
return super.exportXls(request, mesXslWarehouseArea, MesXslWarehouseArea.class, "MES库区管理");
|
||||
QueryWrapper<MesXslWarehouseArea> queryWrapper = QueryGenerator.initQueryWrapper(mesXslWarehouseArea, request.getParameterMap());
|
||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
String selections = request.getParameter("selections");
|
||||
if (oConvertUtils.isNotEmpty(selections)) {
|
||||
queryWrapper.in("id", Arrays.asList(selections.split(",")));
|
||||
}
|
||||
List<MesXslWarehouseArea> exportList = mesXslWarehouseAreaService.list(queryWrapper);
|
||||
mesXslWarehouseAreaService.enrichDisplayedActualCapacity(exportList);
|
||||
ModelAndView mv = new ModelAndView(new JeecgEntityExcelView());
|
||||
mv.addObject(NormalExcelConstants.FILE_NAME, "MES库区管理");
|
||||
mv.addObject(NormalExcelConstants.CLASS, MesXslWarehouseArea.class);
|
||||
ExportParams exportParams = new ExportParams("MES库区管理报表", "导出人:" + sysUser.getRealname(), "MES库区管理", ExcelType.XSSF);
|
||||
exportParams.setImageBasePath(jeecgBaseConfig.getPath().getUpload());
|
||||
mv.addObject(NormalExcelConstants.PARAMS, exportParams);
|
||||
mv.addObject(NormalExcelConstants.DATA_LIST, exportList);
|
||||
String exportFields = request.getParameter(NormalExcelConstants.EXPORT_FIELDS);
|
||||
if (oConvertUtils.isNotEmpty(exportFields)) {
|
||||
mv.addObject(NormalExcelConstants.EXPORT_FIELDS, exportFields);
|
||||
}
|
||||
return mv;
|
||||
}
|
||||
|
||||
@RequiresPermissions("xslmes:mes_xsl_warehouse_area:importExcel")
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.jeecg.modules.xslmes.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 按库区(trim 后编码)聚合剩余数量汇总
|
||||
*/
|
||||
@Data
|
||||
public class MesXslAreaRemainQtySumDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 库区编码(已 trim) */
|
||||
private String areaCode;
|
||||
|
||||
/** 剩余数量合计 */
|
||||
private Long qtySum;
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
package org.jeecg.modules.xslmes.mapper;
|
||||
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.jeecg.modules.xslmes.dto.MesXslAreaRemainQtySumDTO;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: 原材料卡片
|
||||
@@ -10,4 +15,9 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface MesXslRawMaterialCardMapper extends BaseMapper<MesXslRawMaterialCard> {
|
||||
|
||||
/**
|
||||
* 按 trim 后的库区编码汇总原材料卡片剩余数量(当前租户由拦截器补齐)
|
||||
*/
|
||||
List<MesXslAreaRemainQtySumDTO> sumRemainingQtyByTrimmedWarehouseArea(@Param("areaCodes") Collection<String> areaCodes);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
<?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="org.jeecg.modules.xslmes.mapper.MesXslRawMaterialCardMapper">
|
||||
<!-- 库区编码已与卡片 warehouse_area 按 TRIM 对齐(见库区/卡片筛选逻辑) -->
|
||||
<select id="sumRemainingQtyByTrimmedWarehouseArea"
|
||||
resultType="org.jeecg.modules.xslmes.dto.MesXslAreaRemainQtySumDTO">
|
||||
SELECT TRIM(t.warehouse_area) AS areaCode,
|
||||
COALESCE(SUM(COALESCE(t.remaining_quantity, 0)), 0) AS qtySum
|
||||
FROM mes_xsl_raw_material_card t
|
||||
WHERE t.warehouse_area IS NOT NULL
|
||||
AND TRIM(t.warehouse_area) != ''
|
||||
AND TRIM(t.warehouse_area) IN
|
||||
<foreach collection="areaCodes" item="c" open="(" separator="," close=")">
|
||||
#{c}
|
||||
</foreach>
|
||||
GROUP BY TRIM(t.warehouse_area)
|
||||
</select>
|
||||
</mapper>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.jeecg.modules.xslmes.service;
|
||||
|
||||
import org.jeecg.modules.xslmes.vo.MesXslRawMaterialWarehouseBoardVO;
|
||||
|
||||
/**
|
||||
* 原材料库区看板(库区 + 原材料卡片聚合)
|
||||
*/
|
||||
public interface IMesXslRawMaterialWarehouseBoardService {
|
||||
|
||||
/**
|
||||
* 聚合看板数据
|
||||
*
|
||||
* @param warehouseId 所属仓库ID,可空表示全部启用库区
|
||||
* @param keyword 物料名称/条码/批次模糊筛选(可空);非空时仅展示仍有匹配卡片的库区
|
||||
* @param measureType quantity=占用率按剩余数量与 maxCapacity 对齐;weight=按剩余重量
|
||||
*/
|
||||
MesXslRawMaterialWarehouseBoardVO queryBoard(String warehouseId, String keyword, String measureType);
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package org.jeecg.modules.xslmes.service;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* MES 库区管理
|
||||
*/
|
||||
@@ -12,4 +14,9 @@ public interface IMesXslWarehouseAreaService extends IService<MesXslWarehouseAre
|
||||
* 同租户下是否已存在相同库区编码(编辑时传 excludeId 排除自身)
|
||||
*/
|
||||
boolean existsSameAreaCode(String areaCode, String excludeId);
|
||||
|
||||
/**
|
||||
* 按配置策略回填列表中的「实际存放量」展示值(不写库)
|
||||
*/
|
||||
void enrichDisplayedActualCapacity(Collection<MesXslWarehouseArea> areas);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
package org.jeecg.modules.xslmes.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialCardService;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialWarehouseBoardService;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslWarehouseAreaService;
|
||||
import org.jeecg.modules.xslmes.vo.MesXslRawMaterialWarehouseBoardVO;
|
||||
import org.jeecg.modules.xslmes.vo.MesXslRawMaterialWarehouseBoardVO.BoardAreaCardVO;
|
||||
import org.jeecg.modules.xslmes.vo.MesXslRawMaterialWarehouseBoardVO.BoardBandVO;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 原材料库区看板
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MesXslRawMaterialWarehouseBoardServiceImpl implements IMesXslRawMaterialWarehouseBoardService {
|
||||
|
||||
private static final String MEASURE_WEIGHT = "weight";
|
||||
/** 库区编码首段形如 1F、2P、03A 等,仅作看板分组带标题(与仓库分类层级无「楼层」对应关系) */
|
||||
private static final Pattern BAND_FIRST_SEGMENT = Pattern.compile("^\\d+[A-Za-z]+$");
|
||||
|
||||
private final IMesXslWarehouseAreaService mesXslWarehouseAreaService;
|
||||
private final IMesXslRawMaterialCardService mesXslRawMaterialCardService;
|
||||
|
||||
@Override
|
||||
public MesXslRawMaterialWarehouseBoardVO queryBoard(String warehouseId, String keyword, String measureType) {
|
||||
String mt = StringUtils.defaultIfBlank(measureType, "quantity").toLowerCase(Locale.ROOT).trim();
|
||||
if (!MEASURE_WEIGHT.equals(mt)) {
|
||||
mt = "quantity";
|
||||
}
|
||||
MesXslRawMaterialWarehouseBoardVO vo = new MesXslRawMaterialWarehouseBoardVO();
|
||||
vo.setMeasureType(mt);
|
||||
|
||||
QueryWrapper<MesXslWarehouseArea> areaQw = new QueryWrapper<>();
|
||||
areaQw.eq("status", "0");
|
||||
if (StringUtils.isNotBlank(warehouseId)) {
|
||||
areaQw.eq("warehouse_id", warehouseId.trim());
|
||||
}
|
||||
areaQw.orderByAsc("area_code");
|
||||
List<MesXslWarehouseArea> areas = mesXslWarehouseAreaService.list(areaQw);
|
||||
if (areas.isEmpty()) {
|
||||
return vo;
|
||||
}
|
||||
mesXslWarehouseAreaService.enrichDisplayedActualCapacity(areas);
|
||||
|
||||
List<String> areaCodes = areas.stream().map(MesXslWarehouseArea::getAreaCode).filter(StringUtils::isNotBlank).map(String::trim).distinct().collect(Collectors.toList());
|
||||
|
||||
QueryWrapper<MesXslRawMaterialCard> cardQw = new QueryWrapper<>();
|
||||
cardQw.in("warehouse_area", areaCodes);
|
||||
cardQw.apply("(COALESCE(remaining_quantity,0) > 0 OR COALESCE(remaining_weight,0) > 0)");
|
||||
String kw = StringUtils.trimToNull(keyword);
|
||||
if (kw != null) {
|
||||
cardQw.and(w ->
|
||||
w.like("material_name", kw).or().like("barcode", kw).or().like("batch_no", kw));
|
||||
}
|
||||
List<MesXslRawMaterialCard> cards = mesXslRawMaterialCardService.list(cardQw);
|
||||
|
||||
Map<String, List<MesXslRawMaterialCard>> byArea =
|
||||
cards.stream().filter(c -> StringUtils.isNotBlank(c.getWarehouseArea())).collect(Collectors.groupingBy(c -> c.getWarehouseArea().trim(), Collectors.toList()));
|
||||
|
||||
boolean filterByKeyword = kw != null;
|
||||
Map<String, BoardBandVO> bandMap = new LinkedHashMap<>();
|
||||
|
||||
for (MesXslWarehouseArea area : areas) {
|
||||
String code = StringUtils.trimToEmpty(area.getAreaCode());
|
||||
if (code.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
List<MesXslRawMaterialCard> list = byArea.getOrDefault(code, new ArrayList<>());
|
||||
if (filterByKeyword && list.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
BoardAreaCardVO cardVo = aggregateArea(area, list, mt);
|
||||
BandKey bk = resolveBand(area.getAreaCode());
|
||||
BoardBandVO band = bandMap.computeIfAbsent(bk.key, k -> {
|
||||
BoardBandVO b = new BoardBandVO();
|
||||
b.setBandKey(bk.key);
|
||||
b.setBandLabel(bk.label);
|
||||
b.setBandSort(bk.sort);
|
||||
return b;
|
||||
});
|
||||
band.getAreas().add(cardVo);
|
||||
}
|
||||
|
||||
List<BoardBandVO> bands =
|
||||
bandMap.values().stream().sorted(Comparator.comparing(BoardBandVO::getBandSort, Comparator.nullsLast(Integer::compareTo)).thenComparing(BoardBandVO::getBandKey, Comparator.nullsLast(String::compareTo))).collect(Collectors.toList());
|
||||
|
||||
for (BoardBandVO band : bands) {
|
||||
band.getAreas().sort(Comparator.comparing(BoardAreaCardVO::getAreaCode, Comparator.nullsLast(String::compareTo)));
|
||||
}
|
||||
vo.setBands(bands);
|
||||
return vo;
|
||||
}
|
||||
|
||||
private BoardAreaCardVO aggregateArea(MesXslWarehouseArea area, List<MesXslRawMaterialCard> list, String measureType) {
|
||||
BoardAreaCardVO vo = new BoardAreaCardVO();
|
||||
vo.setAreaId(area.getId());
|
||||
vo.setAreaCode(area.getAreaCode());
|
||||
vo.setAreaName(area.getAreaName());
|
||||
vo.setWarehouseId(area.getWarehouseId());
|
||||
vo.setWarehouseName(area.getWarehouseName());
|
||||
vo.setMaxCapacity(area.getMaxCapacity());
|
||||
vo.setActualCapacity(area.getActualCapacity());
|
||||
vo.setCardCount(list.size());
|
||||
|
||||
int sumQty = 0;
|
||||
BigDecimal sumW = BigDecimal.ZERO;
|
||||
LinkedHashSet<String> materialNames = new LinkedHashSet<>();
|
||||
for (MesXslRawMaterialCard c : list) {
|
||||
if (c.getRemainingQuantity() != null) {
|
||||
sumQty += c.getRemainingQuantity();
|
||||
}
|
||||
if (c.getRemainingWeight() != null) {
|
||||
sumW = sumW.add(c.getRemainingWeight());
|
||||
}
|
||||
if (StringUtils.isNotBlank(c.getMaterialName())) {
|
||||
materialNames.add(c.getMaterialName().trim());
|
||||
}
|
||||
}
|
||||
vo.setCurrentQuantity(sumQty);
|
||||
vo.setCurrentWeight(sumW.setScale(3, RoundingMode.HALF_UP));
|
||||
vo.setMaterialKindCount(materialNames.size());
|
||||
vo.setTopMaterialNames(materialNames.stream().limit(8).collect(Collectors.toList()));
|
||||
|
||||
Double usage = null;
|
||||
Integer maxCap = area.getMaxCapacity();
|
||||
if (maxCap != null && maxCap > 0) {
|
||||
if (MEASURE_WEIGHT.equals(measureType)) {
|
||||
usage = sumW.multiply(BigDecimal.valueOf(100)).divide(BigDecimal.valueOf(maxCap), 1, RoundingMode.HALF_UP).doubleValue();
|
||||
} else {
|
||||
usage = BigDecimal.valueOf(sumQty).multiply(BigDecimal.valueOf(100)).divide(BigDecimal.valueOf(maxCap), 1, RoundingMode.HALF_UP).doubleValue();
|
||||
}
|
||||
if (usage > 100) {
|
||||
usage = 100d;
|
||||
}
|
||||
if (usage < 0) {
|
||||
usage = 0d;
|
||||
}
|
||||
}
|
||||
vo.setUsagePercent(usage);
|
||||
vo.setAlertLevel(resolveAlert(usage, list.isEmpty()));
|
||||
return vo;
|
||||
}
|
||||
|
||||
private static String resolveAlert(Double usagePercent, boolean emptyCards) {
|
||||
if (emptyCards) {
|
||||
return "empty";
|
||||
}
|
||||
if (usagePercent == null) {
|
||||
return "unknown";
|
||||
}
|
||||
if (usagePercent <= 0) {
|
||||
return "empty";
|
||||
}
|
||||
if (usagePercent < 30) {
|
||||
return "low";
|
||||
}
|
||||
if (usagePercent < 90) {
|
||||
return "normal";
|
||||
}
|
||||
if (usagePercent < 100) {
|
||||
return "high";
|
||||
}
|
||||
return "full";
|
||||
}
|
||||
|
||||
private BandKey resolveBand(String areaCode) {
|
||||
if (StringUtils.isBlank(areaCode)) {
|
||||
return new BandKey("zzz_other", "其它", 999_000);
|
||||
}
|
||||
String trimmed = areaCode.trim();
|
||||
String head = trimmed.contains("-") ? trimmed.substring(0, trimmed.indexOf('-')) : trimmed;
|
||||
if (BAND_FIRST_SEGMENT.matcher(head).matches()) {
|
||||
String normalized = normalizeBandToken(head);
|
||||
int sort = bandSort(normalized);
|
||||
return new BandKey(normalized.toLowerCase(Locale.ROOT), normalized, sort);
|
||||
}
|
||||
String label = StringUtils.left(head, 16);
|
||||
if (label.isEmpty()) {
|
||||
label = trimmed.length() > 16 ? trimmed.substring(0, 16) : trimmed;
|
||||
}
|
||||
return new BandKey("z_" + label.toLowerCase(Locale.ROOT), label, 888_000 + Math.abs(label.hashCode() % 1000));
|
||||
}
|
||||
|
||||
private static String normalizeBandToken(String raw) {
|
||||
if (raw == null) {
|
||||
return "其它";
|
||||
}
|
||||
String s = raw.toUpperCase(Locale.ROOT);
|
||||
s = s.replace('F', 'F').replace('P', 'P');
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单排序:按前缀中的数字升序(1F<2F<10F),末尾字母次之
|
||||
*/
|
||||
private static int bandSort(String label) {
|
||||
String digits = "";
|
||||
String tail = "";
|
||||
for (int i = 0; i < label.length(); i++) {
|
||||
char ch = label.charAt(i);
|
||||
if (Character.isDigit(ch)) {
|
||||
digits += ch;
|
||||
} else {
|
||||
tail = label.substring(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
int num = 0;
|
||||
try {
|
||||
if (!digits.isEmpty()) {
|
||||
num = Integer.parseInt(digits);
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
num = 0;
|
||||
}
|
||||
return num * 100 + Math.min(Math.abs(tail.hashCode() % 99), 99);
|
||||
}
|
||||
|
||||
private static final class BandKey {
|
||||
final String key;
|
||||
final String label;
|
||||
final int sort;
|
||||
|
||||
BandKey(String key, String label, int sort) {
|
||||
this.key = key;
|
||||
this.label = label;
|
||||
this.sort = sort;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,25 @@
|
||||
package org.jeecg.modules.xslmes.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.modules.xslmes.capacity.WarehouseAreaDisplayedActualCapacityMediator;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
|
||||
import org.jeecg.modules.xslmes.mapper.MesXslWarehouseAreaMapper;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslWarehouseAreaService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* MES 库区管理
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MesXslWarehouseAreaServiceImpl extends ServiceImpl<MesXslWarehouseAreaMapper, MesXslWarehouseArea> implements IMesXslWarehouseAreaService {
|
||||
|
||||
private final WarehouseAreaDisplayedActualCapacityMediator displayedActualCapacityMediator;
|
||||
|
||||
@Override
|
||||
public boolean existsSameAreaCode(String areaCode, String excludeId) {
|
||||
if (StringUtils.isBlank(areaCode)) {
|
||||
@@ -24,4 +31,9 @@ public class MesXslWarehouseAreaServiceImpl extends ServiceImpl<MesXslWarehouseA
|
||||
.ne(StringUtils.isNotBlank(excludeId), MesXslWarehouseArea::getId, excludeId)
|
||||
.count() > 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrichDisplayedActualCapacity(Collection<MesXslWarehouseArea> areas) {
|
||||
displayedActualCapacityMediator.apply(areas);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,11 +41,11 @@ public class MesXslWarehouseServiceImpl extends ServiceImpl<MesXslWarehouseMappe
|
||||
return;
|
||||
}
|
||||
String code = baseMapper.queryCategoryCodeById(e.getWarehouseCategory());
|
||||
if (!MesXslWarehouseCategory.CUSTOMER_CATEGORY_CODE.equals(code)) {
|
||||
if (!MesXslWarehouseCategory.isCustomerWarehouseCategoryCode(code)) {
|
||||
e.setCustomerId(null);
|
||||
e.setCustomerShortName(null);
|
||||
}
|
||||
if (!MesXslWarehouseCategory.SUPPLIER_CATEGORY_CODE.equals(code)) {
|
||||
if (!MesXslWarehouseCategory.isSupplierWarehouseCategoryCode(code)) {
|
||||
e.setSupplierId(null);
|
||||
e.setSupplierShortName(null);
|
||||
}
|
||||
@@ -56,11 +56,11 @@ public class MesXslWarehouseServiceImpl extends ServiceImpl<MesXslWarehouseMappe
|
||||
return;
|
||||
}
|
||||
String code = baseMapper.queryCategoryCodeById(e.getWarehouseCategory());
|
||||
if (MesXslWarehouseCategory.CUSTOMER_CATEGORY_CODE.equals(code)) {
|
||||
if (MesXslWarehouseCategory.isCustomerWarehouseCategoryCode(code)) {
|
||||
if (oConvertUtils.isEmpty(e.getCustomerId())) {
|
||||
throw new JeecgBootException("仓库分类为客户库时,请选择客户");
|
||||
}
|
||||
} else if (MesXslWarehouseCategory.SUPPLIER_CATEGORY_CODE.equals(code)) {
|
||||
} else if (MesXslWarehouseCategory.isSupplierWarehouseCategoryCode(code)) {
|
||||
if (oConvertUtils.isEmpty(e.getSupplierId())) {
|
||||
throw new JeecgBootException("仓库分类为供应商库时,请选择供应商");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package org.jeecg.modules.xslmes.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 原材料库区看板:按库区编码前缀等规则形成的「分组带」汇总展示(不按业务楼层拆分)
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "原材料库区看板")
|
||||
public class MesXslRawMaterialWarehouseBoardVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "计量口径回显:quantity=按剩余数量 weight=按剩余重量")
|
||||
private String measureType;
|
||||
|
||||
@Schema(description = "分组带列表")
|
||||
private List<BoardBandVO> bands = new ArrayList<>();
|
||||
|
||||
@Data
|
||||
@Schema(description = "看板分组带(如 1F、2F)")
|
||||
public static class BoardBandVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "分组键(排序用)")
|
||||
private String bandKey;
|
||||
|
||||
@Schema(description = "分组标题")
|
||||
private String bandLabel;
|
||||
|
||||
@Schema(description = "排序号,越小越靠前")
|
||||
private Integer bandSort;
|
||||
|
||||
@Schema(description = "该组下的库区卡片")
|
||||
private List<BoardAreaCardVO> areas = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Data
|
||||
@Schema(description = "单个库区卡片数据")
|
||||
public static class BoardAreaCardVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "库区主键")
|
||||
private String areaId;
|
||||
|
||||
@Schema(description = "库区编码")
|
||||
private String areaCode;
|
||||
|
||||
@Schema(description = "库区名称")
|
||||
private String areaName;
|
||||
|
||||
@Schema(description = "所属仓库ID")
|
||||
private String warehouseId;
|
||||
|
||||
@Schema(description = "所属仓库名称")
|
||||
private String warehouseName;
|
||||
|
||||
@Schema(description = "最大存放量(库区主数据)")
|
||||
private Integer maxCapacity;
|
||||
|
||||
@Schema(description = "实际存放量(库区主数据快照)")
|
||||
private Integer actualCapacity;
|
||||
|
||||
@Schema(description = "在库卡片张数(有剩余量)")
|
||||
private Integer cardCount;
|
||||
|
||||
@Schema(description = "物料种类数(去重物料名称)")
|
||||
private Integer materialKindCount;
|
||||
|
||||
@Schema(description = "剩余数量合计")
|
||||
private Integer currentQuantity;
|
||||
|
||||
@Schema(description = "剩余重量合计")
|
||||
private BigDecimal currentWeight;
|
||||
|
||||
@Schema(description = "物料名称摘要(前若干种)")
|
||||
private List<String> topMaterialNames = new ArrayList<>();
|
||||
|
||||
@Schema(description = "占用率 0~100,无上限或上限为0时为 null")
|
||||
private Double usagePercent;
|
||||
|
||||
@Schema(description = "告警级别:empty/low/normal/high/full/unknown")
|
||||
private String alertLevel;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ spring:
|
||||
application:
|
||||
name: jeecg-system
|
||||
config:
|
||||
import: optional:classpath:config/application-liteflow.yml
|
||||
import:
|
||||
- optional:classpath:config/application-liteflow.yml
|
||||
- optional:classpath:config/application-xslmes-warehouse-area.yml
|
||||
profiles:
|
||||
active: '@profile.name@'
|
||||
@@ -0,0 +1,11 @@
|
||||
# MES XSL — 库区「实际存放量」展示回填(不写库,可覆盖)
|
||||
xslmes:
|
||||
warehouse-area:
|
||||
display-actual-capacity:
|
||||
enabled: true
|
||||
# 按「原材料库」分类编码解析 sys_category.id(推荐;业务树已无楼层语义,编码中 F1/F2 后缀若变化请改此处)
|
||||
raw-material-warehouse-category-codes:
|
||||
- XSLMES_WH_F1_YCL
|
||||
- XSLMES_WH_F2_YCL
|
||||
# 可选:直接写 warehouse_category=id,与上面的编码解析结果合并
|
||||
raw-material-warehouse-category-ids: []
|
||||
@@ -0,0 +1,19 @@
|
||||
-- 原材料库区看板:菜单与查询权限(父菜单 MES XSL 1900000000000000300)
|
||||
|
||||
INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`)
|
||||
SELECT '1900000000000000580', '1900000000000000300', '原材料库区看板', '/xslmes/mesXslRawMaterialWarehouseBoard', 'xslmes/mesXslRawMaterialWarehouseBoard/MesXslRawMaterialWarehouseBoard', 1, NULL, NULL, 1, NULL, '0', 12.50, 0, 'ant-design:layout-outlined', 0, 1, 0, 0, '按库区聚合展示原材料卡片库存', 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000580');
|
||||
|
||||
INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`)
|
||||
SELECT '1900000000000000581', '1900000000000000580', '查询', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_raw_material_warehouse_board:list', '1', 1.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000581');
|
||||
|
||||
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
|
||||
SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000580', NULL, NOW(), '127.0.0.1'
|
||||
FROM `sys_role` r WHERE r.`role_code` = 'admin'
|
||||
AND NOT EXISTS (SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.id AND rp.`permission_id` = '1900000000000000580');
|
||||
|
||||
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
|
||||
SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000581', NULL, NOW(), '127.0.0.1'
|
||||
FROM `sys_role` r WHERE r.`role_code` = 'admin'
|
||||
AND NOT EXISTS (SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.id AND rp.`permission_id` = '1900000000000000581');
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
enum Api {
|
||||
board = '/xslmes/mesXslRawMaterialWarehouseBoard/board',
|
||||
}
|
||||
|
||||
export const fetchBoard = (params: { warehouseId?: string; keyword?: string; measureType?: string }) =>
|
||||
defHttp.get({ url: Api.board, params });
|
||||
@@ -0,0 +1,725 @@
|
||||
<template>
|
||||
<div class="rmb-page">
|
||||
<div class="rmb-toolbar card-surface">
|
||||
<div class="rmb-toolbar-row">
|
||||
<div class="rmb-title">
|
||||
<span class="rmb-title-icon" />
|
||||
<div>
|
||||
<div class="rmb-title-text">原材料库区看板</div>
|
||||
<div class="rmb-title-sub">按库区聚合条码卡片,点击查看明细</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rmb-actions">
|
||||
<a-button type="primary" ghost @click="loadBoard">
|
||||
<Icon icon="ant-design:reload-outlined" />
|
||||
刷新
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rmb-filter" role="search">
|
||||
<div class="rmb-filter-row">
|
||||
<div class="rmb-inline-field">
|
||||
<span class="rmb-inline-label">所属仓库</span>
|
||||
<a-select
|
||||
v-model:value="warehouseId"
|
||||
allow-clear
|
||||
placeholder="全部仓库"
|
||||
class="rmb-filter-control"
|
||||
:loading="warehouseLoading"
|
||||
:options="warehouseOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="rmb-inline-field">
|
||||
<span class="rmb-inline-label">物料 / 条码 / 批次</span>
|
||||
<a-input
|
||||
v-model:value="keyword"
|
||||
placeholder="关键字模糊筛选"
|
||||
class="rmb-filter-control"
|
||||
allow-clear
|
||||
@press-enter="loadBoard"
|
||||
/>
|
||||
</div>
|
||||
<div class="rmb-inline-field">
|
||||
<span class="rmb-inline-label">占用率口径</span>
|
||||
<a-radio-group v-model:value="measureType" button-style="solid" @change="loadBoard">
|
||||
<a-radio-button value="quantity">剩余数量</a-radio-button>
|
||||
<a-radio-button value="weight">剩余重量</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<div class="rmb-inline-field rmb-inline-actions">
|
||||
<a-button type="primary" @click="loadBoard">查询</a-button>
|
||||
<a-button @click="resetFilter">重置</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-spin :spinning="loading">
|
||||
<div v-if="!bands.length && !loading" class="rmb-empty card-surface">
|
||||
<a-empty description="暂无启用库区或无匹配数据" />
|
||||
</div>
|
||||
|
||||
<div v-for="band in bands" :key="band.bandKey" class="rmb-band card-surface">
|
||||
<div class="rmb-band-head">
|
||||
<span class="rmb-band-label">{{ band.bandLabel }}</span>
|
||||
<span class="rmb-band-count">{{ band.areas?.length || 0 }} 个库区</span>
|
||||
</div>
|
||||
<div class="rmb-card-row">
|
||||
<div
|
||||
v-for="area in band.areas"
|
||||
:key="area.areaId"
|
||||
class="rmb-card"
|
||||
:class="'rmb-card--' + (area.alertLevel || 'unknown')"
|
||||
@click="openDetail(area)"
|
||||
>
|
||||
<div class="rmb-card-head">
|
||||
<span class="rmb-card-code">{{ area.areaCode }}</span>
|
||||
<a-tag v-if="area.warehouseName" color="processing" class="rmb-card-wh">{{ area.warehouseName }}</a-tag>
|
||||
</div>
|
||||
<div class="rmb-card-name">{{ area.areaName || area.areaCode }}</div>
|
||||
<div class="rmb-card-stats">
|
||||
<div>
|
||||
<div class="rmb-stat-label">当前(数量)</div>
|
||||
<div class="rmb-stat-value">{{ area.currentQuantity ?? 0 }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="rmb-stat-label">当前(重量)</div>
|
||||
<div class="rmb-stat-value">{{ formatWeight(area.currentWeight) }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="rmb-stat-label">上限</div>
|
||||
<div class="rmb-stat-value">{{ area.maxCapacity ?? '—' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="progressPercent(area)"
|
||||
:show-info="true"
|
||||
:stroke-color="progressStroke(area)"
|
||||
:trail-color="'rgba(0,0,0,0.06)'"
|
||||
size="small"
|
||||
/>
|
||||
<div class="rmb-card-foot">
|
||||
<span class="rmb-meta">卡片 {{ area.cardCount ?? 0 }} 张 · 物料 {{ area.materialKindCount ?? 0 }} 种</span>
|
||||
</div>
|
||||
<div class="rmb-tags">
|
||||
<template v-for="(m, idx) in (area.topMaterialNames || []).slice(0, 4)" :key="idx">
|
||||
<a-tooltip :title="m">
|
||||
<a-tag class="rmb-tag">{{ truncate(m, 10) }}</a-tag>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-tag v-if="(area.topMaterialNames?.length || 0) > 4" class="rmb-tag rmb-tag-more">+{{ (area.topMaterialNames?.length || 0) - 4 }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
|
||||
<a-drawer
|
||||
v-model:open="detailOpen"
|
||||
:title="detailTitle"
|
||||
placement="right"
|
||||
width="min(96vw, 1080px)"
|
||||
destroy-on-close
|
||||
@close="onDetailClose"
|
||||
@after-open-change="onDrawerAfterOpenChange"
|
||||
>
|
||||
<BasicTable @register="registerDetailTable" />
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="xslmes-mesXslRawMaterialWarehouseBoard" setup>
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { BasicTable, useTable } from '/@/components/Table';
|
||||
import type { BasicColumn } from '/@/components/Table';
|
||||
import type { FormSchema } from '/@/components/Form';
|
||||
import Icon from '/@/components/Icon';
|
||||
import { fetchBoard } from './MesXslRawMaterialWarehouseBoard.api';
|
||||
import { list as cardList } from '/@/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCard.api';
|
||||
import { list as warehouseList } from '/@/views/xslmes/mesXslWarehouse/MesXslWarehouse.api';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { getTenantId } from '/@/utils/auth';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
/** 上次选择的「所属仓库」持久化键(区分租户) */
|
||||
const LS_RM_BOARD_WAREHOUSE = 'MES_XSL_RM_BOARD_WAREHOUSE_ID';
|
||||
|
||||
function warehouseBoardStorageKey() {
|
||||
const tid = getTenantId();
|
||||
if (tid === undefined || tid === null || tid === '') {
|
||||
return LS_RM_BOARD_WAREHOUSE;
|
||||
}
|
||||
return `${LS_RM_BOARD_WAREHOUSE}_${tid}`;
|
||||
}
|
||||
|
||||
/** 写入 localStorage:空串/undefined 则清除 */
|
||||
function persistWarehouseId(id: string | undefined | null) {
|
||||
const k = warehouseBoardStorageKey();
|
||||
if (id === undefined || id === null || String(id).trim() === '') {
|
||||
localStorage.removeItem(k);
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(k, String(id).trim());
|
||||
}
|
||||
|
||||
/** 读取上次选择的仓库 id(若无或非法由调用方忽略) */
|
||||
function readPersistedWarehouseId(): string | undefined {
|
||||
const v = localStorage.getItem(warehouseBoardStorageKey());
|
||||
const t = v?.trim();
|
||||
return t || undefined;
|
||||
}
|
||||
|
||||
interface BoardArea {
|
||||
areaId: string;
|
||||
areaCode: string;
|
||||
areaName?: string;
|
||||
warehouseId?: string;
|
||||
warehouseName?: string;
|
||||
maxCapacity?: number | null;
|
||||
actualCapacity?: number | null;
|
||||
cardCount?: number;
|
||||
materialKindCount?: number;
|
||||
currentQuantity?: number;
|
||||
currentWeight?: number | string | null;
|
||||
topMaterialNames?: string[];
|
||||
usagePercent?: number | null;
|
||||
alertLevel?: string;
|
||||
}
|
||||
|
||||
interface BoardBand {
|
||||
bandKey: string;
|
||||
bandLabel: string;
|
||||
bandSort?: number;
|
||||
areas: BoardArea[];
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const warehouseLoading = ref(false);
|
||||
const measureType = ref<'quantity' | 'weight'>('quantity');
|
||||
const warehouseId = ref<string | undefined>(undefined);
|
||||
const keyword = ref('');
|
||||
const bands = ref<BoardBand[]>([]);
|
||||
|
||||
const warehouseOptions = ref<{ label: string; value: string }[]>([]);
|
||||
|
||||
const detailOpen = ref(false);
|
||||
const detailAreaCode = ref('');
|
||||
const detailTitle = computed(() => (detailAreaCode.value ? `库区明细 · ${detailAreaCode.value}` : '库区明细'));
|
||||
|
||||
const detailQuery = reactive({ warehouseArea: '' });
|
||||
|
||||
/** 库区明细抽屉:条码/批次/物料 合并关键字 + 剩余数量筛选(单行紧凑布局) */
|
||||
const detailFormColResponsive = {
|
||||
xs: 24,
|
||||
sm: 24,
|
||||
md: 9,
|
||||
lg: 9,
|
||||
xl: 9,
|
||||
xxl: 9,
|
||||
span: 9,
|
||||
} as const;
|
||||
const detailQtyColResponsive = {
|
||||
xs: 24,
|
||||
sm: 24,
|
||||
md: 7,
|
||||
lg: 7,
|
||||
xl: 7,
|
||||
xxl: 7,
|
||||
span: 7,
|
||||
} as const;
|
||||
|
||||
const detailSearchFormSchema: FormSchema[] = [
|
||||
{
|
||||
label: '条码/批次/物料',
|
||||
field: 'mixKeyword',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '条码/批次/物料模糊',
|
||||
allowClear: true,
|
||||
},
|
||||
colProps: { ...detailFormColResponsive },
|
||||
},
|
||||
{
|
||||
label: '剩余数量',
|
||||
field: 'remainQtyFilter',
|
||||
component: 'Select',
|
||||
defaultValue: '',
|
||||
componentProps: {
|
||||
placeholder: '全部',
|
||||
allowClear: true,
|
||||
style: { width: '100%', maxWidth: 200 },
|
||||
options: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '有剩余', value: 'has' },
|
||||
{ label: '无剩余', value: 'none' },
|
||||
],
|
||||
},
|
||||
colProps: { ...detailQtyColResponsive },
|
||||
},
|
||||
];
|
||||
|
||||
/** 查询、重置与本行输入框并排:前两列占 16 栅格,操作列占 8,合计 24 */
|
||||
const detailFormActionCol = { xs: 24, sm: 24, md: 8, lg: 8, xl: 8, xxl: 8, span: 8 };
|
||||
|
||||
const detailColumns: BasicColumn[] = [
|
||||
{ title: '条码', dataIndex: 'barcode', width: 190 },
|
||||
{ title: '批次号', dataIndex: 'batchNo', width: 160 },
|
||||
{ title: '入场日期', dataIndex: 'entryDate', width: 110 },
|
||||
{ title: '物料名称', dataIndex: 'materialName', width: 140 },
|
||||
{ title: '剩余数量', dataIndex: 'remainingQuantity', width: 90 },
|
||||
{ title: '剩余重量', dataIndex: 'remainingWeight', width: 90 },
|
||||
{ title: '检测结果', dataIndex: 'testResult_dictText', width: 90 },
|
||||
{ title: '库区', dataIndex: 'warehouseArea', width: 100 },
|
||||
];
|
||||
|
||||
const [registerDetailTable, { reload }] = useTable({
|
||||
title: '原材料卡片明细',
|
||||
api: cardList,
|
||||
columns: detailColumns,
|
||||
useSearchForm: true,
|
||||
formConfig: {
|
||||
labelWidth: 108,
|
||||
layout: 'horizontal',
|
||||
compact: true,
|
||||
schemas: detailSearchFormSchema,
|
||||
autoSubmitOnEnter: true,
|
||||
showAdvancedButton: false,
|
||||
submitButtonOptions: { text: '查询' },
|
||||
resetButtonOptions: { text: '重置' },
|
||||
actionColOptions: {
|
||||
...detailFormActionCol,
|
||||
style: { textAlign: 'left', whiteSpace: 'nowrap', paddingLeft: '4px' },
|
||||
},
|
||||
},
|
||||
showTableSetting: true,
|
||||
canResize: true,
|
||||
immediate: false,
|
||||
pagination: { pageSize: 20 },
|
||||
beforeFetch: (params) => {
|
||||
const merged = Object.assign({}, params, {
|
||||
warehouseArea: (detailQuery.warehouseArea || '').trim(),
|
||||
});
|
||||
const v = merged.remainQtyFilter;
|
||||
// 后端只认 remainQtyFilter=has/none;空串不传避免误筛
|
||||
if (v === '' || v === undefined || v === null) {
|
||||
delete merged.remainQtyFilter;
|
||||
}
|
||||
const kw = merged.mixKeyword;
|
||||
if (kw === '' || kw === undefined || kw === null || String(kw).trim() === '') {
|
||||
delete merged.mixKeyword;
|
||||
} else if (typeof kw === 'string') {
|
||||
merged.mixKeyword = kw.trim();
|
||||
}
|
||||
return merged;
|
||||
},
|
||||
});
|
||||
|
||||
async function loadWarehouses() {
|
||||
warehouseLoading.value = true;
|
||||
try {
|
||||
const res = await warehouseList({ pageNo: 1, pageSize: 500 });
|
||||
const records = res?.records ?? [];
|
||||
warehouseOptions.value = records.map((r: Record<string, string>) => ({
|
||||
label: r.warehouseName || r.warehouseCode || r.id,
|
||||
value: r.id,
|
||||
}));
|
||||
// 恢复上次选择的仓库(仅当仍在列表中)
|
||||
const savedId = readPersistedWarehouseId();
|
||||
if (savedId) {
|
||||
const ok = warehouseOptions.value.some((o) => o.value === savedId);
|
||||
if (ok) {
|
||||
warehouseId.value = savedId;
|
||||
} else {
|
||||
localStorage.removeItem(warehouseBoardStorageKey());
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
createMessage.warning('加载仓库列表失败');
|
||||
} finally {
|
||||
warehouseLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBoard() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await fetchBoard({
|
||||
warehouseId: warehouseId.value,
|
||||
keyword: keyword.value?.trim(),
|
||||
measureType: measureType.value,
|
||||
});
|
||||
bands.value = (data?.bands as BoardBand[]) || [];
|
||||
if (!(data?.bands?.length ?? 0)) {
|
||||
bands.value = [];
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
createMessage.error(e instanceof Error ? e.message : '加载看板失败');
|
||||
bands.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function resetFilter() {
|
||||
warehouseId.value = undefined;
|
||||
persistWarehouseId(undefined);
|
||||
keyword.value = '';
|
||||
measureType.value = 'quantity';
|
||||
loadBoard();
|
||||
}
|
||||
|
||||
function progressPercent(area: BoardArea): number {
|
||||
const p = area.usagePercent;
|
||||
if (p == null || Number.isNaN(p)) {
|
||||
return 0;
|
||||
}
|
||||
return Math.min(100, Math.max(0, Math.round(p)));
|
||||
}
|
||||
|
||||
function progressStroke(area: BoardArea) {
|
||||
const level = area.alertLevel;
|
||||
const map: Record<string, string> = {
|
||||
empty: '#bfbfbf',
|
||||
low: '#597ef7',
|
||||
normal: 'var(--j-global-primary-color, #1677ff)',
|
||||
high: '#fa8c16',
|
||||
full: '#f5222d',
|
||||
unknown: '#8c8c8c',
|
||||
};
|
||||
return map[level || 'unknown'] || map.unknown;
|
||||
}
|
||||
|
||||
function formatWeight(w: BoardArea['currentWeight']) {
|
||||
if (w == null || w === '') {
|
||||
return '—';
|
||||
}
|
||||
const n = Number(w);
|
||||
if (Number.isFinite(n)) {
|
||||
return n.toFixed(3);
|
||||
}
|
||||
return String(w);
|
||||
}
|
||||
|
||||
function truncate(s: string, n: number) {
|
||||
return s.length > n ? `${s.slice(0, n)}…` : s;
|
||||
}
|
||||
|
||||
function openDetail(area: BoardArea) {
|
||||
const code = String(area.areaCode ?? '').trim();
|
||||
detailQuery.warehouseArea = code;
|
||||
detailAreaCode.value = code;
|
||||
detailOpen.value = true;
|
||||
// 不在此处 reload:抽屉 destroy-on-close 时子表格可能尚未挂载,会导致请求未带上条件或无实例
|
||||
}
|
||||
|
||||
/** 抽屉打开动画完成后再拉取明细,确保 BasicTable 已 register */
|
||||
async function onDrawerAfterOpenChange(open: boolean) {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
const code = String(detailQuery.warehouseArea || '').trim();
|
||||
if (!code) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
await reload();
|
||||
}
|
||||
|
||||
function onDetailClose() {
|
||||
detailQuery.warehouseArea = '';
|
||||
}
|
||||
|
||||
// 用户切换「所属仓库」时持久化(无 immediate,避免挂载前误清空缓存)
|
||||
watch(warehouseId, (v) => {
|
||||
persistWarehouseId(v);
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await loadWarehouses();
|
||||
await loadBoard();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.rmb-page {
|
||||
padding: 0 8px 16px;
|
||||
min-height: calc(100vh - 120px);
|
||||
background: linear-gradient(180deg, rgba(22, 119, 255, 0.06) 0%, transparent 480px),
|
||||
radial-gradient(1200px 400px at 10% -10%, rgba(82, 196, 26, 0.08), transparent);
|
||||
}
|
||||
|
||||
.card-surface {
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||||
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.rmb-toolbar {
|
||||
padding: 16px 20px 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.rmb-toolbar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.rmb-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.rmb-title-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, var(--j-global-primary-color, #1677ff), #722ed1);
|
||||
box-shadow: 0 6px 16px rgba(22, 119, 255, 0.35);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rmb-title-text {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.rmb-title-sub {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.rmb-filter-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px 20px;
|
||||
}
|
||||
|
||||
.rmb-inline-field {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
/* 标签右对齐 + 固定宽度,与控件纵向居中对齐 */
|
||||
.rmb-inline-label {
|
||||
flex: 0 0 148px;
|
||||
width: 148px;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
line-height: 32px;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.rmb-filter-control {
|
||||
width: 220px !important;
|
||||
}
|
||||
|
||||
/* 按钮组与其它字段同一中线,不靠虚构标签占位 */
|
||||
.rmb-inline-actions {
|
||||
gap: 8px;
|
||||
padding-left: 4px;
|
||||
margin-left: 4px;
|
||||
border-left: 1px solid rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.rmb-inline-label {
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.rmb-inline-field {
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rmb-filter-control {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.rmb-inline-actions {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
.rmb-empty {
|
||||
padding: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.rmb-band {
|
||||
padding: 16px 16px 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.rmb-band-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px dashed rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.rmb-band-label {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.rmb-band-count {
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.rmb-card-row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 14px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 6px;
|
||||
scroll-snap-type: x proximity;
|
||||
}
|
||||
|
||||
.rmb-card {
|
||||
flex: 0 0 280px;
|
||||
scroll-snap-align: start;
|
||||
padding: 14px 14px 12px;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease;
|
||||
border: 1px solid rgba(22, 119, 255, 0.12);
|
||||
background: linear-gradient(145deg, #ffffff 0%, #f6faff 100%);
|
||||
}
|
||||
|
||||
.rmb-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
background: var(--j-global-primary-color, #1677ff);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.rmb-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 12px 28px rgba(22, 119, 255, 0.18);
|
||||
}
|
||||
|
||||
.rmb-card--empty::before {
|
||||
background: #bfbfbf;
|
||||
}
|
||||
.rmb-card--low::before {
|
||||
background: #597ef7;
|
||||
}
|
||||
.rmb-card--normal::before {
|
||||
background: var(--j-global-primary-color, #1677ff);
|
||||
}
|
||||
.rmb-card--high::before {
|
||||
background: #fa8c16;
|
||||
}
|
||||
.rmb-card--full::before {
|
||||
background: #f5222d;
|
||||
}
|
||||
.rmb-card--unknown::before {
|
||||
background: #8c8c8c;
|
||||
}
|
||||
|
||||
.rmb-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.rmb-card-code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.rmb-card-wh {
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rmb-card-name {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
margin-bottom: 10px;
|
||||
min-height: 18px;
|
||||
}
|
||||
|
||||
.rmb-card-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.rmb-stat-label {
|
||||
font-size: 11px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.rmb-stat-value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.rmb-card-foot {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.rmb-meta {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.rmb-tags {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.rmb-tag {
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.rmb-tag-more {
|
||||
border-style: dashed;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user