feat(aps): 新增排产单明细合并功能及优化界面展示

- 在排产单明细表格中添加多选合并功能
- 实现排产单明细合并对话框及合并逻辑
- 优化排产单明细表格列配置和表单布局
- 添加合并校验和接收产需单API接口
- 重构订单绑定解绑逻辑提升用户体验
- 添加ScheduleDetailCoilBind组件引入
This commit is contained in:
2026-06-29 16:07:29 +08:00
parent da01bfaa48
commit ee376f922f
19 changed files with 1547 additions and 154 deletions

View File

@@ -18,8 +18,11 @@ import com.klp.common.core.validate.EditGroup;
import com.klp.common.enums.BusinessType; import com.klp.common.enums.BusinessType;
import com.klp.common.utils.poi.ExcelUtil; import com.klp.common.utils.poi.ExcelUtil;
import com.klp.flow.domain.vo.SchProdScheduleItemVo; import com.klp.flow.domain.vo.SchProdScheduleItemVo;
import com.klp.flow.domain.vo.SchProdScheduleItemMergeValidateVo;
import com.klp.flow.domain.bo.SchProdScheduleItemBo; import com.klp.flow.domain.bo.SchProdScheduleItemBo;
import com.klp.flow.domain.bo.SchProdScheduleItemMergeBo; import com.klp.flow.domain.bo.SchProdScheduleItemMergeBo;
import com.klp.flow.domain.bo.SchProdScheduleItemReceiveBo;
import com.klp.flow.domain.bo.SchProdScheduleItemMergeValidateBo;
import com.klp.flow.service.ISchProdScheduleItemService; import com.klp.flow.service.ISchProdScheduleItemService;
import com.klp.common.core.page.TableDataInfo; import com.klp.common.core.page.TableDataInfo;
@@ -117,4 +120,22 @@ public class SchProdScheduleItemController extends BaseController {
public R<Void> merge(@Validated @RequestBody SchProdScheduleItemMergeBo mergeBo) { public R<Void> merge(@Validated @RequestBody SchProdScheduleItemMergeBo mergeBo) {
return toAjax(iSchProdScheduleItemService.mergeByBo(mergeBo)); return toAjax(iSchProdScheduleItemService.mergeByBo(mergeBo));
} }
/**
* 接收产需单:从 sch_prod_schedule + sch_prod_schedule_detail 全字段复制到 sch_prod_schedule_item
*/
@Log(title = "排产单主加明细可合并", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping("/receive")
public R<Void> receive(@Validated @RequestBody SchProdScheduleItemReceiveBo receiveBo) {
return toAjax(iSchProdScheduleItemService.receiveByBo(receiveBo));
}
/**
* 合并校验:检查待合并记录来源产需单的 header 字段是否一致
*/
@PostMapping("/mergeValidate")
public R<SchProdScheduleItemMergeValidateVo> mergeValidate(@Validated @RequestBody SchProdScheduleItemMergeValidateBo validateBo) {
return R.ok(iSchProdScheduleItemService.validateMerge(validateBo));
}
} }

View File

@@ -128,13 +128,9 @@ public class SchProdScheduleItem extends BaseEntity {
*/ */
private String returnReason; private String returnReason;
/** /**
* 排产单主表ID * 排产明细关联主表ID逗号分隔未合并存单值合并后存多个
*/ */
private String scheduleIds; private String scheduleDetailIds;
/**
* 来源销售订单明细ID溯源原始订单规格
*/
private String orderDetailIds;
/** /**
* 规格 例1.0X1250 * 规格 例1.0X1250
*/ */

View File

@@ -154,14 +154,9 @@ public class SchProdScheduleItemBo extends BaseEntity {
private String returnReason; private String returnReason;
/** /**
* 排产单主表ID * 排产明细关联主表ID逗号分隔未合并存单值合并后存多个
*/ */
private String scheduleIds; private String scheduleDetailIds;
/**
* 来源销售订单明细ID溯源原始订单规格
*/
private String orderDetailIds;
/** /**
* 规格 例1.0X1250 * 规格 例1.0X1250

View File

@@ -0,0 +1,22 @@
package com.klp.flow.domain.bo;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import java.util.List;
/**
* 排产单合并校验请求对象
*
* @author klp
* @date 2026-06-29
*/
@Data
public class SchProdScheduleItemMergeValidateBo {
/**
* 待合并记录ID列表
*/
@NotEmpty(message = "待合并记录ID不能为空")
private List<Long> ids;
}

View File

@@ -0,0 +1,21 @@
package com.klp.flow.domain.bo;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 产需单接收请求对象
*
* @author klp
* @date 2026-06-29
*/
@Data
public class SchProdScheduleItemReceiveBo {
/**
* 排产单主表ID产需单ID
*/
@NotNull(message = "排产单主表ID不能为空")
private Long scheduleId;
}

View File

@@ -0,0 +1,115 @@
package com.klp.flow.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 排产单合并校验视图对象
*
* @author klp
* @date 2026-06-29
*/
@Data
public class SchProdScheduleItemMergeValidateVo {
/** 是否所有 header 字段一致 */
private Boolean consistent;
/** 有差异的字段列表 */
private List<FieldDiff> diffs;
/** 被合并记录快照 */
private List<ItemInfo> items;
/** 汇总信息 */
private MergedSummary summary;
/**
* 字段差异
*/
@Data
public static class FieldDiff {
/** 字段名Java属性名 */
private String fieldName;
/** 字段中文标签 */
private String fieldLabel;
/** scheduleId → 值 */
private Map<String, Object> values;
}
/**
* 被合并记录快照
*/
@Data
public static class ItemInfo {
/** sch_prod_schedule_item 主键 */
private Long itemId;
/** 第一个源 schedule_id从 scheduleIds 解析) */
private Long sourceScheduleId;
/** 排产单号 */
private String scheduleNo;
/** 订货单位 */
private String customerName;
/** 品名 */
private String productType;
/** 规格 */
private String spec;
/** 材质 */
private String material;
/** 排产吨数 */
private BigDecimal scheduleWeight;
/** 来源排产明细ID串 */
private String scheduleDetailIds;
/** 业务员 */
private String businessUser;
/** 联系电话 */
private String businessPhone;
/** 交货期(天) */
private Long deliveryCycle;
/** 产品用途 */
private String usePurpose;
/** 厚度公差 */
private String thicknessTolerance;
/** 宽度公差 */
private String widthTolerance;
/** 表面质量 */
private String surfaceQuality;
/** 表面处理 */
private String surfaceTreatment;
/** 内径尺寸 */
private String innerDiameter;
/** 外径要求 */
private String outerDiameter;
/** 包装要求 */
private String packReq;
/** 切边要求 */
private String cutEdgeReq;
/** 单件重量 */
private String singleCoilWeight;
/** 交货重量偏差 */
private String weightDeviation;
/** 其他技术要求 */
private String otherTechReq;
/** 付款情况说明 */
private String paymentDesc;
/** 关联销售合同号 */
private String relContractNo;
/** 排产总计划吨数 */
private BigDecimal totalPlanWeight;
/** 订单日期 */
private java.util.Date orderDate;
}
/**
* 合并汇总
*/
@Data
public static class MergedSummary {
/** 明细条数 */
private int itemCount;
/** 合计排产吨数 */
private BigDecimal totalScheduleWeight;
}
}

View File

@@ -8,6 +8,7 @@ import com.alibaba.excel.annotation.ExcelProperty;
import com.klp.common.annotation.ExcelDictFormat; import com.klp.common.annotation.ExcelDictFormat;
import com.klp.common.convert.ExcelDictConvert; import com.klp.common.convert.ExcelDictConvert;
import lombok.Data; import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
/** /**
@@ -40,6 +41,8 @@ public class SchProdScheduleItemVo {
*/ */
@ExcelProperty(value = "生产日期", converter = ExcelDictConvert.class) @ExcelProperty(value = "生产日期", converter = ExcelDictConvert.class)
@ExcelDictFormat(readConverterExp = "和=合同号组成业务关联键") @ExcelDictFormat(readConverterExp = "和=合同号组成业务关联键")
@JsonFormat(pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date prodDate; private Date prodDate;
/** /**
@@ -181,17 +184,10 @@ public class SchProdScheduleItemVo {
private String returnReason; private String returnReason;
/** /**
* 排产单主表ID * 排产明细关联主表ID逗号分隔
*/ */
@ExcelProperty(value = "排产主表ID") @ExcelProperty(value = "排产明细关联主表ID")
private String scheduleIds; private String scheduleDetailIds;
/**
* 来源销售订单明细ID溯源原始订单规格
*/
@ExcelProperty(value = "来源销售订单明细ID", converter = ExcelDictConvert.class)
@ExcelDictFormat(readConverterExp = "溯=源原始订单规格")
private String orderDetailIds;
/** /**
* 规格 例1.0X1250 * 规格 例1.0X1250

View File

@@ -2,8 +2,11 @@ package com.klp.flow.service;
import com.klp.flow.domain.SchProdScheduleItem; import com.klp.flow.domain.SchProdScheduleItem;
import com.klp.flow.domain.vo.SchProdScheduleItemVo; import com.klp.flow.domain.vo.SchProdScheduleItemVo;
import com.klp.flow.domain.vo.SchProdScheduleItemMergeValidateVo;
import com.klp.flow.domain.bo.SchProdScheduleItemBo; import com.klp.flow.domain.bo.SchProdScheduleItemBo;
import com.klp.flow.domain.bo.SchProdScheduleItemMergeBo; import com.klp.flow.domain.bo.SchProdScheduleItemMergeBo;
import com.klp.flow.domain.bo.SchProdScheduleItemReceiveBo;
import com.klp.flow.domain.bo.SchProdScheduleItemMergeValidateBo;
import com.klp.common.core.page.TableDataInfo; import com.klp.common.core.page.TableDataInfo;
import com.klp.common.core.domain.PageQuery; import com.klp.common.core.domain.PageQuery;
@@ -57,4 +60,14 @@ public interface ISchProdScheduleItemService {
* 合并多条排产单主加明细可合并记录:先删除被合并的记录,再插入合并后的新记录 * 合并多条排产单主加明细可合并记录:先删除被合并的记录,再插入合并后的新记录
*/ */
Boolean mergeByBo(SchProdScheduleItemMergeBo mergeBo); Boolean mergeByBo(SchProdScheduleItemMergeBo mergeBo);
/**
* 接收产需单:从 sch_prod_schedule + sch_prod_schedule_detail 全字段复制到 sch_prod_schedule_item
*/
Boolean receiveByBo(SchProdScheduleItemReceiveBo receiveBo);
/**
* 合并校验:检查待合并记录来源产需单的 header 字段是否一致
*/
SchProdScheduleItemMergeValidateVo validateMerge(SchProdScheduleItemMergeValidateBo validateBo);
} }

View File

@@ -11,16 +11,27 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.klp.flow.domain.bo.SchProdScheduleItemBo; import com.klp.flow.domain.bo.SchProdScheduleItemBo;
import com.klp.flow.domain.bo.SchProdScheduleItemMergeBo; import com.klp.flow.domain.bo.SchProdScheduleItemMergeBo;
import com.klp.flow.domain.bo.SchProdScheduleItemReceiveBo;
import com.klp.flow.domain.bo.SchProdScheduleItemMergeValidateBo;
import com.klp.flow.domain.vo.SchProdScheduleItemVo; import com.klp.flow.domain.vo.SchProdScheduleItemVo;
import com.klp.flow.domain.vo.SchProdScheduleItemMergeValidateVo;
import com.klp.flow.domain.SchProdScheduleItem; import com.klp.flow.domain.SchProdScheduleItem;
import com.klp.flow.domain.SchProdSchedule;
import com.klp.flow.domain.SchProdScheduleDetail;
import com.klp.flow.mapper.SchProdScheduleItemMapper; import com.klp.flow.mapper.SchProdScheduleItemMapper;
import com.klp.flow.mapper.SchProdScheduleMapper;
import com.klp.flow.mapper.SchProdScheduleDetailMapper;
import com.klp.flow.service.ISchProdScheduleItemService; import com.klp.flow.service.ISchProdScheduleItemService;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Collection; import java.util.Collection;
import java.util.stream.Collectors;
/** /**
* 排产单主加明细可合并Service业务层处理 * 排产单主加明细可合并Service业务层处理
@@ -33,6 +44,8 @@ import java.util.Collection;
public class SchProdScheduleItemServiceImpl implements ISchProdScheduleItemService { public class SchProdScheduleItemServiceImpl implements ISchProdScheduleItemService {
private final SchProdScheduleItemMapper baseMapper; private final SchProdScheduleItemMapper baseMapper;
private final SchProdScheduleMapper schProdScheduleMapper;
private final SchProdScheduleDetailMapper schProdScheduleDetailMapper;
/** /**
* 查询排产单主加明细可合并 * 查询排产单主加明细可合并
@@ -89,8 +102,7 @@ public class SchProdScheduleItemServiceImpl implements ISchProdScheduleItemServi
lqw.eq(StringUtils.isNotBlank(bo.getOtherTechReq()), SchProdScheduleItem::getOtherTechReq, bo.getOtherTechReq()); lqw.eq(StringUtils.isNotBlank(bo.getOtherTechReq()), SchProdScheduleItem::getOtherTechReq, bo.getOtherTechReq());
lqw.eq(StringUtils.isNotBlank(bo.getPaymentDesc()), SchProdScheduleItem::getPaymentDesc, bo.getPaymentDesc()); lqw.eq(StringUtils.isNotBlank(bo.getPaymentDesc()), SchProdScheduleItem::getPaymentDesc, bo.getPaymentDesc());
lqw.eq(StringUtils.isNotBlank(bo.getReturnReason()), SchProdScheduleItem::getReturnReason, bo.getReturnReason()); lqw.eq(StringUtils.isNotBlank(bo.getReturnReason()), SchProdScheduleItem::getReturnReason, bo.getReturnReason());
lqw.eq(StringUtils.isNotBlank(bo.getScheduleIds()), SchProdScheduleItem::getScheduleIds, bo.getScheduleIds()); lqw.eq(StringUtils.isNotBlank(bo.getScheduleDetailIds()), SchProdScheduleItem::getScheduleDetailIds, bo.getScheduleDetailIds());
lqw.eq(StringUtils.isNotBlank(bo.getOrderDetailIds()), SchProdScheduleItem::getOrderDetailIds, bo.getOrderDetailIds());
lqw.eq(StringUtils.isNotBlank(bo.getSpec()), SchProdScheduleItem::getSpec, bo.getSpec()); lqw.eq(StringUtils.isNotBlank(bo.getSpec()), SchProdScheduleItem::getSpec, bo.getSpec());
lqw.eq(StringUtils.isNotBlank(bo.getMaterial()), SchProdScheduleItem::getMaterial, bo.getMaterial()); lqw.eq(StringUtils.isNotBlank(bo.getMaterial()), SchProdScheduleItem::getMaterial, bo.getMaterial());
lqw.eq(bo.getScheduleWeight() != null, SchProdScheduleItem::getScheduleWeight, bo.getScheduleWeight()); lqw.eq(bo.getScheduleWeight() != null, SchProdScheduleItem::getScheduleWeight, bo.getScheduleWeight());
@@ -179,4 +191,287 @@ public class SchProdScheduleItemServiceImpl implements ISchProdScheduleItemServi
} }
return flag; return flag;
} }
/**
* 接收产需单:从 sch_prod_schedule + sch_prod_schedule_detail 全字段复制到 sch_prod_schedule_item
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean receiveByBo(SchProdScheduleItemReceiveBo receiveBo) {
Long scheduleId = receiveBo.getScheduleId();
// 1. 查询产需单主表
SchProdSchedule header = schProdScheduleMapper.selectById(scheduleId);
if (header == null) {
throw new RuntimeException("产需单不存在scheduleId=" + scheduleId);
}
// 2. 查询产需单明细列表
List<SchProdScheduleDetail> details = schProdScheduleDetailMapper.selectList(
Wrappers.<SchProdScheduleDetail>lambdaQuery()
.eq(SchProdScheduleDetail::getScheduleId, scheduleId)
);
if (details == null || details.isEmpty()) {
throw new RuntimeException("产需单无明细数据scheduleId=" + scheduleId);
}
// 3. 遍历每条 detail构建 SchProdScheduleItem 列表
List<SchProdScheduleItem> addList = new ArrayList<>(details.size());
for (SchProdScheduleDetail detail : details) {
SchProdScheduleItem item = new SchProdScheduleItem();
// 从 header 复制所有字段
item.setScheduleNo(header.getScheduleNo());
item.setProdDate(header.getProdDate());
item.setScheduleStatus(2L); // 已下达
item.setTotalPlanWeight(header.getTotalPlanWeight());
item.setRelContractNo(header.getRelContractNo());
item.setBusinessUser(header.getBusinessUser());
item.setBusinessPhone(header.getBusinessPhone());
item.setOrderDate(header.getOrderDate());
item.setCustomerName(header.getCustomerName());
item.setDeliveryCycle(header.getDeliveryCycle());
item.setUsePurpose(header.getUsePurpose());
item.setProductType(header.getProductType());
item.setThicknessTolerance(header.getThicknessTolerance());
item.setWidthTolerance(header.getWidthTolerance());
item.setSurfaceQuality(header.getSurfaceQuality());
item.setSurfaceTreatment(header.getSurfaceTreatment());
item.setInnerDiameter(header.getInnerDiameter());
item.setOuterDiameter(header.getOuterDiameter());
item.setPackReq(header.getPackReq());
item.setCutEdgeReq(header.getCutEdgeReq());
item.setSingleCoilWeight(header.getSingleCoilWeight());
item.setWeightDeviation(header.getWeightDeviation());
item.setOtherTechReq(header.getOtherTechReq());
item.setPaymentDesc(header.getPaymentDesc());
item.setRemark(header.getRemark());
// 不复制 returnReason
// 从 detail 复制
item.setSpec(detail.getSpec());
item.setMaterial(detail.getMaterial());
item.setScheduleWeight(detail.getScheduleWeight());
item.setProductItem(detail.getProductType());
item.setRowRemark(detail.getRemark());
// 来源追溯未合并各存明细ID
item.setScheduleDetailIds(String.valueOf(detail.getScheduleDetailId()));
validEntityBeforeSave(item);
addList.add(item);
}
// 4. 批量插入
baseMapper.insertBatch(addList);
// 5. 更新产需单状态为 2已下达
header.setScheduleStatus(2L);
schProdScheduleMapper.updateById(header);
return true;
}
/**
* 合并校验:检查待合并记录来源产需单的 header 字段是否一致
*/
@Override
public SchProdScheduleItemMergeValidateVo validateMerge(SchProdScheduleItemMergeValidateBo validateBo) {
List<Long> ids = validateBo.getIds();
// 1. 查询所有待合并的 sch_prod_schedule_item 记录
List<SchProdScheduleItem> items = baseMapper.selectBatchIds(ids);
if (items == null || items.size() < 2) {
throw new RuntimeException("待合并记录不足至少需要2条");
}
// 2. 解析每条记录的 scheduleIds逗号分隔取第一个作为源 schedule_id
// 查询所有源 sch_prod_schedule 记录
List<Long> sourceScheduleIds = new ArrayList<>();
for (SchProdScheduleItem item : items) {
String sids = item.getScheduleDetailIds();
if (sids != null && !sids.isEmpty()) {
String[] parts = sids.split(",");
if (parts.length > 0) {
String firstId = parts[0].trim();
if (!firstId.isEmpty()) {
sourceScheduleIds.add(Long.valueOf(firstId));
}
}
}
}
List<Long> distinctSourceIds = sourceScheduleIds.stream().distinct().collect(Collectors.toList());
List<SchProdSchedule> sourceHeaders = schProdScheduleMapper.selectBatchIds(distinctSourceIds);
Map<Long, SchProdSchedule> headerMap = new LinkedHashMap<>();
for (SchProdSchedule h : sourceHeaders) {
headerMap.put(h.getScheduleId(), h);
}
// 3. 定义要比较的字段列表(字段名 → 中文标签)
Map<String, String> compareFields = new LinkedHashMap<>();
compareFields.put("scheduleNo", "排产单号");
compareFields.put("customerName", "订货单位");
compareFields.put("businessUser", "业务员");
compareFields.put("businessPhone", "联系电话");
compareFields.put("deliveryCycle", "交货期(天)");
compareFields.put("usePurpose", "产品用途");
compareFields.put("productType", "品名");
compareFields.put("thicknessTolerance", "厚度公差");
compareFields.put("widthTolerance", "宽度公差");
compareFields.put("surfaceQuality", "表面质量");
compareFields.put("surfaceTreatment", "表面处理");
compareFields.put("innerDiameter", "内径尺寸");
compareFields.put("outerDiameter", "外径要求");
compareFields.put("packReq", "包装要求");
compareFields.put("cutEdgeReq", "切边要求");
compareFields.put("singleCoilWeight", "单件重量");
compareFields.put("weightDeviation", "交货重量偏差");
compareFields.put("otherTechReq", "其他技术要求");
compareFields.put("paymentDesc", "付款情况说明");
compareFields.put("relContractNo", "关联销售合同号");
compareFields.put("totalPlanWeight", "排产总计划吨数");
compareFields.put("orderDate", "订单日期");
// 4. 逐一比较字段
List<SchProdScheduleItemMergeValidateVo.FieldDiff> diffs = new ArrayList<>();
boolean allConsistent = true;
for (Map.Entry<String, String> entry : compareFields.entrySet()) {
String fieldName = entry.getKey();
String fieldLabel = entry.getValue();
Map<String, Object> values = new LinkedHashMap<>();
for (SchProdSchedule header : sourceHeaders) {
Long headerId = header.getScheduleId();
Object val = getFieldValue(header, fieldName);
values.put(String.valueOf(headerId), val);
}
// 判断所有值是否一致
boolean same = values.values().stream().distinct().count() <= 1;
if (!same) {
allConsistent = false;
SchProdScheduleItemMergeValidateVo.FieldDiff diff = new SchProdScheduleItemMergeValidateVo.FieldDiff();
diff.setFieldName(fieldName);
diff.setFieldLabel(fieldLabel);
diff.setValues(values);
diffs.add(diff);
}
}
// 4.1 比对明细级字段spec, material, productItem—— 这些字段来自 item 自身,不来自 header
Map<String, String> itemCompareFields = new LinkedHashMap<>();
itemCompareFields.put("spec", "规格");
itemCompareFields.put("material", "材质");
itemCompareFields.put("productItem", "品名项");
for (Map.Entry<String, String> entry : itemCompareFields.entrySet()) {
String fieldName = entry.getKey();
String fieldLabel = entry.getValue();
Map<String, Object> values = new LinkedHashMap<>();
for (SchProdScheduleItem item : items) {
Object val = getItemFieldValue(item, fieldName);
values.put(String.valueOf(item.getScheduleId()), val);
}
boolean same = values.values().stream().distinct().count() <= 1;
if (!same) {
allConsistent = false;
SchProdScheduleItemMergeValidateVo.FieldDiff diff = new SchProdScheduleItemMergeValidateVo.FieldDiff();
diff.setFieldName(fieldName);
diff.setFieldLabel(fieldLabel);
diff.setValues(values);
diffs.add(diff);
}
}
// 5. 构建 ItemInfo 快照列表
List<SchProdScheduleItemMergeValidateVo.ItemInfo> itemInfos = new ArrayList<>();
BigDecimal totalWeight = BigDecimal.ZERO;
for (SchProdScheduleItem item : items) {
SchProdScheduleItemMergeValidateVo.ItemInfo info = new SchProdScheduleItemMergeValidateVo.ItemInfo();
info.setItemId(item.getScheduleId());
// 解析第一个源 schedule_id
String sids = item.getScheduleDetailIds();
Long firstSourceId = null;
if (sids != null && !sids.isEmpty()) {
String[] parts = sids.split(",");
if (parts.length > 0) {
try {
firstSourceId = Long.valueOf(parts[0].trim());
} catch (NumberFormatException ignored) {}
}
}
info.setSourceScheduleId(firstSourceId);
info.setScheduleNo(item.getScheduleNo());
info.setCustomerName(item.getCustomerName());
info.setProductType(item.getProductType());
info.setSpec(item.getSpec());
info.setMaterial(item.getMaterial());
info.setScheduleWeight(item.getScheduleWeight());
info.setScheduleDetailIds(item.getScheduleDetailIds());
// header 字段
info.setBusinessUser(item.getBusinessUser());
info.setBusinessPhone(item.getBusinessPhone());
info.setDeliveryCycle(item.getDeliveryCycle());
info.setUsePurpose(item.getUsePurpose());
info.setThicknessTolerance(item.getThicknessTolerance());
info.setWidthTolerance(item.getWidthTolerance());
info.setSurfaceQuality(item.getSurfaceQuality());
info.setSurfaceTreatment(item.getSurfaceTreatment());
info.setInnerDiameter(item.getInnerDiameter());
info.setOuterDiameter(item.getOuterDiameter());
info.setPackReq(item.getPackReq());
info.setCutEdgeReq(item.getCutEdgeReq());
info.setSingleCoilWeight(item.getSingleCoilWeight());
info.setWeightDeviation(item.getWeightDeviation());
info.setOtherTechReq(item.getOtherTechReq());
info.setPaymentDesc(item.getPaymentDesc());
info.setRelContractNo(item.getRelContractNo());
info.setTotalPlanWeight(item.getTotalPlanWeight());
info.setOrderDate(item.getOrderDate());
itemInfos.add(info);
totalWeight = totalWeight.add(item.getScheduleWeight() != null ? item.getScheduleWeight() : BigDecimal.ZERO);
}
// 6. 构建汇总
SchProdScheduleItemMergeValidateVo.MergedSummary summary = new SchProdScheduleItemMergeValidateVo.MergedSummary();
summary.setItemCount(items.size());
summary.setTotalScheduleWeight(totalWeight);
// 7. 组装返回
SchProdScheduleItemMergeValidateVo vo = new SchProdScheduleItemMergeValidateVo();
vo.setConsistent(allConsistent);
vo.setDiffs(diffs);
vo.setItems(itemInfos);
vo.setSummary(summary);
return vo;
}
/**
* 反射获取字段值(用于动态字段比较 — SchProdSchedule header
*/
private Object getFieldValue(SchProdSchedule header, String fieldName) {
try {
java.lang.reflect.Field field = SchProdSchedule.class.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(header);
} catch (Exception e) {
return null;
}
}
/**
* 反射获取字段值(用于动态字段比较 — SchProdScheduleItem 明细)
*/
private Object getItemFieldValue(SchProdScheduleItem item, String fieldName) {
try {
java.lang.reflect.Field field = SchProdScheduleItem.class.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(item);
} catch (Exception e) {
return null;
}
}
} }

View File

@@ -196,6 +196,8 @@ public class SchProdScheduleServiceImpl implements ISchProdScheduleService {
lqw.eq(StringUtils.isNotBlank(bo.getOtherTechReq()), SchProdSchedule::getOtherTechReq, bo.getOtherTechReq()); lqw.eq(StringUtils.isNotBlank(bo.getOtherTechReq()), SchProdSchedule::getOtherTechReq, bo.getOtherTechReq());
lqw.eq(StringUtils.isNotBlank(bo.getPaymentDesc()), SchProdSchedule::getPaymentDesc, bo.getPaymentDesc()); lqw.eq(StringUtils.isNotBlank(bo.getPaymentDesc()), SchProdSchedule::getPaymentDesc, bo.getPaymentDesc());
lqw.eq(StringUtils.isNotBlank(bo.getReturnReason()), SchProdSchedule::getReturnReason, bo.getReturnReason()); lqw.eq(StringUtils.isNotBlank(bo.getReturnReason()), SchProdSchedule::getReturnReason, bo.getReturnReason());
// 根据创建时间倒叙排序
lqw.orderByDesc(SchProdSchedule::getCreateTime);
return lqw; return lqw;
} }

View File

@@ -0,0 +1,27 @@
import request from '@/utils/request'
// 查询排产明细-钢卷绑定关系列表
export function listDetailCoilRel(query) {
return request({
url: '/flow/detailCoilRel/list',
method: 'get',
params: query
})
}
// 批量新增绑定关系
export function batchAddDetailCoilRel(data) {
return request({
url: '/flow/detailCoilRel/batch',
method: 'post',
data
})
}
// 删除绑定关系
export function delDetailCoilRel(relIds) {
return request({
url: '/flow/detailCoilRel/' + relIds,
method: 'delete'
})
}

View File

@@ -70,3 +70,32 @@ export function delScheduleItem(ids) {
method: 'delete' method: 'delete'
}) })
} }
// ====== 排产单明细项 接收/合并 ======
// 接收产需单(后端全字段复制)
export function receiveScheduleItem(scheduleId) {
return request({
url: '/flow/prodScheduleItem/receive',
method: 'post',
data: { scheduleId }
})
}
// 合并验证(检查源排产单字段一致性)
export function validateMerge(data) {
return request({
url: '/flow/prodScheduleItem/mergeValidate',
method: 'post',
data
})
}
// 合并排产单明细项
export function mergeScheduleItem(data) {
return request({
url: '/flow/prodScheduleItem/merge',
method: 'post',
data
})
}

View File

@@ -0,0 +1,334 @@
<template>
<span>
<el-button icon="el-icon-link" size="mini" @click="open">绑定排产明细</el-button>
<el-dialog title="排产明细 - 钢卷绑定" :visible.sync="visible" width="1200px" append-to-body :close-on-click-modal="false"
v-loading="loading">
<!-- 上半部分今日排产明细 -->
<div class="bind-section">
<div class="bind-section-header">今日排产明细{{ scheduleItems.length }} </div>
<el-table :data="scheduleItems" highlight-current-row @row-click="handleDetailClick" max-height="300"
size="small">
<el-table-column prop="scheduleNo" label="排产单号" min-width="130" show-overflow-tooltip />
<el-table-column prop="prodDate" label="生产日期" width="90" />
<el-table-column prop="spec" label="规格" width="90" show-overflow-tooltip />
<el-table-column prop="material" label="材质" width="70" />
<el-table-column prop="scheduleWeight" label="排产吨数" width="80" />
<el-table-column prop="productType" label="品名" min-width="100" show-overflow-tooltip />
<el-table-column prop="customerName" label="订货单位" min-width="100" show-overflow-tooltip />
<el-table-column label="已绑钢卷" width="80">
<template slot-scope="scope">
<el-tag size="mini" type="info">{{ scope.row._boundCount || 0 }} </el-tag>
</template>
</el-table-column>
</el-table>
</div>
<!-- 下半部分已绑定钢卷 -->
<div class="bind-section" v-if="currentDetail" style="margin-top:16px;">
<div class="bind-section-header">
已绑定钢卷 {{ currentDetail.spec }} / {{ currentDetail.material }}
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openCoilSelect">绑定钢卷</el-button>
</div>
<el-table :data="boundCoils" size="small" v-loading="boundCoilsLoading">
<el-table-column prop="currentCoilNo" label="钢卷号" width="140" show-overflow-tooltip />
<el-table-column prop="specification" label="规格" width="100" show-overflow-tooltip />
<el-table-column prop="material" label="材质" width="70" />
<el-table-column prop="netWeight" label="净重" width="80" />
<el-table-column prop="_remark" label="备注" min-width="100" show-overflow-tooltip />
<el-table-column label="操作" width="70" align="center">
<template slot-scope="scope">
<el-button type="text" style="color:#ff4d4f;" size="mini" @click="handleUnbind(scope.row)">解绑</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="boundCoils.length === 0 && !boundCoilsLoading" description="暂未绑定钢卷" />
</div>
<el-empty v-if="scheduleItems.length === 0 && !loading" description="今日无排产明细" />
</el-dialog>
<!-- 选卷子弹窗 -->
<el-dialog title="选择钢卷" :visible.sync="coilDialogVisible" width="900px" append-to-body
:close-on-click-modal="false">
<div style="display:flex;gap:8px;margin-bottom:10px;">
<el-input v-model="coilQuery.keyword" placeholder="钢卷号 / 入场卷号" size="small" style="width:220px"
clearable @keyup.enter.native="searchCoils" />
<el-button size="small" type="primary" icon="el-icon-search" @click="searchCoils">搜索</el-button>
<span style="margin-left:auto;font-size:12px;color:#909399;line-height:32px;">
已选 {{ selectedCoils.length }}
</span>
</div>
<el-table :data="coilList" @selection-change="handleCoilSelection" ref="coilSelectTable" size="small"
v-loading="coilListLoading" max-height="400">
<el-table-column type="selection" width="50" :selectable="checkCoilSelectable" />
<el-table-column prop="currentCoilNo" label="钢卷号" width="140" show-overflow-tooltip />
<el-table-column prop="enterCoilNo" label="入场卷号" width="140" show-overflow-tooltip />
<el-table-column prop="specification" label="规格" width="100" show-overflow-tooltip />
<el-table-column prop="material" label="材质"/>
<el-table-column prop="netWeight" label="净重" width="80" />
<el-table-column prop="warehouseName" label="仓库" width="80" show-overflow-tooltip />
</el-table>
<pagination v-show="coilTotal > 0" :total="coilTotal" :page.sync="coilQuery.pageNum"
:limit.sync="coilQuery.pageSize" @pagination="searchCoils" style="padding:10px 0 0 0;" />
<span slot="footer">
<el-button type="primary" @click="confirmBindCoils" :loading="bindLoading"
:disabled="selectedCoils.length === 0">确定绑定{{ selectedCoils.length }} </el-button>
<el-button @click="coilDialogVisible = false">取消</el-button>
</span>
</el-dialog>
</span>
</template>
<script>
import { listScheduleItem } from '@/api/aps/schedule'
import { listMaterialCoil } from '@/api/wms/coil'
import { listDetailCoilRel, batchAddDetailCoilRel, delDetailCoilRel } from '@/api/aps/detailCoilRel'
export default {
name: 'ScheduleDetailCoilBind',
data() {
return {
visible: false,
loading: false,
scheduleItems: [],
currentDetail: null,
boundCoils: [],
boundCoilsLoading: false,
allBoundCoilIds: new Set(),
coilDialogVisible: false,
coilListLoading: false,
coilList: [],
coilTotal: 0,
selectedCoils: [],
bindLoading: false,
coilQuery: {
keyword: '',
pageNum: 1,
pageSize: 20
}
}
},
computed: {
prodDate() {
return new Date().toISOString().slice(0, 10)
}
},
methods: {
open() {
this.visible = true
this.currentDetail = null
this.boundCoils = []
this.loadScheduleItems()
},
async loadScheduleItems() {
this.loading = true
try {
const res = await listScheduleItem({ prodDate: this.prodDate })
this.scheduleItems = (res.rows || []).map(item => {
item._boundCount = 0
return item
})
// 加载所有已绑定的钢卷ID
await this.loadAllBoundCoilIds()
} catch (e) {
this.scheduleItems = []
} finally {
this.loading = false
}
},
async loadAllBoundCoilIds() {
const ids = this.scheduleItems.map(i => i.id || i.scheduleDetailId).filter(Boolean)
if (ids.length === 0) return
try {
// 尝试批量查询
const res = await listDetailCoilRel({ scheduleDetailIds: ids.join(',') })
const rows = res.rows || []
this.allBoundCoilIds = new Set(rows.map(r => r.coilId))
this.applyBoundCounts(rows)
} catch (e) {
// 批量查询失败则逐个查询
try {
const allRows = []
const tasks = ids.map(id =>
listDetailCoilRel({ scheduleDetailId: id }).then(r => {
(r.rows || []).forEach(row => allRows.push(row))
}).catch(() => {})
)
await Promise.all(tasks)
this.allBoundCoilIds = new Set(allRows.map(r => r.coilId))
this.applyBoundCounts(allRows)
} catch (e2) {
// ignore
}
}
},
applyBoundCounts(rows) {
const countMap = {}
rows.forEach(r => {
const key = r.scheduleDetailId
countMap[key] = (countMap[key] || 0) + 1
})
this.scheduleItems.forEach(item => {
const key = item.id || item.scheduleDetailId
item._boundCount = countMap[key] || 0
})
},
async handleDetailClick(row) {
this.currentDetail = row
this.boundCoilsLoading = true
try {
const detailId = row.id || row.scheduleDetailId
const res = await listDetailCoilRel({ scheduleDetailId: detailId })
const rels = res.rows || []
if (rels.length > 0) {
const coilIds = rels.map(r => r.coilId).filter(Boolean)
const coilRes = await listMaterialCoil({
pageNum: 1,
pageSize: 200,
coilIds: coilIds.join(',')
})
const coilMap = {}
if (coilRes.rows) {
coilRes.rows.forEach(c => { coilMap[c.coilId || c.id] = c })
}
this.boundCoils = rels.map(r => ({
...r,
...(coilMap[r.coilId] || {}),
_remark: r.remark || ''
}))
} else {
this.boundCoils = []
}
} catch (e) {
this.boundCoils = []
} finally {
this.boundCoilsLoading = false
}
},
async openCoilSelect() {
this.coilDialogVisible = true
this.selectedCoils = []
this.coilQuery.keyword = ''
this.coilQuery.pageNum = 1
await this.searchCoils()
},
async searchCoils() {
this.coilListLoading = true
try {
const params = {
pageNum: this.coilQuery.pageNum,
pageSize: this.coilQuery.pageSize,
status: 0,
dataType: 1,
exclusiveStatus: 0
}
if (this.coilQuery.keyword) {
params.currentCoilNo = this.coilQuery.keyword
}
const res = await listMaterialCoil(params)
// 过滤掉已绑定的钢卷
this.coilList = (res.rows || []).filter(c => !this.allBoundCoilIds.has(c.coilId || c.id))
this.coilTotal = res.total || 0
} catch (e) {
this.coilList = []
} finally {
this.coilListLoading = false
}
},
checkCoilSelectable(row) {
return !this.allBoundCoilIds.has(row.coilId || row.id)
},
handleCoilSelection(selection) {
this.selectedCoils = selection
},
async confirmBindCoils() {
if (this.selectedCoils.length === 0) {
this.$message.warning('请至少选择一个钢卷')
return
}
this.bindLoading = true
try {
const detailId = this.currentDetail.id || this.currentDetail.scheduleDetailId
const data = this.selectedCoils.map(c => ({
scheduleDetailId: detailId,
coilId: c.coilId || c.id,
remark: ''
}))
await batchAddDetailCoilRel(data)
this.$modal.msgSuccess(`成功绑定 ${this.selectedCoils.length}`)
this.coilDialogVisible = false
// 记录新绑定的钢卷ID
this.selectedCoils.forEach(c => {
this.allBoundCoilIds.add(c.coilId || c.id)
})
// 更新当前明细的已绑卷数
if (this.currentDetail) {
this.currentDetail._boundCount = (this.currentDetail._boundCount || 0) + this.selectedCoils.length
}
// 刷新已绑定钢卷列表
await this.handleDetailClick(this.currentDetail)
} catch (e) {
this.$modal.msgError('绑定失败')
} finally {
this.bindLoading = false
}
},
async handleUnbind(row) {
try {
await this.$confirm('确认解绑该钢卷吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const relId = row.relId || row.id
await delDetailCoilRel(relId)
this.$modal.msgSuccess('解绑成功')
// 从已绑定集合中移除
if (row.coilId) {
this.allBoundCoilIds.delete(row.coilId)
}
if (this.currentDetail) {
this.currentDetail._boundCount = Math.max(0, (this.currentDetail._boundCount || 0) - 1)
}
await this.handleDetailClick(this.currentDetail)
} catch (e) {
if (e !== 'cancel') {
this.$modal.msgError('解绑失败')
}
}
}
}
}
</script>
<style scoped lang="scss">
.bind-section {
border: 1px solid #e4e7ed;
border-radius: 4px;
overflow: hidden;
}
.bind-section-header {
background: #f5f7fa;
padding: 8px 12px;
font-size: 13px;
font-weight: 600;
color: #303133;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e4e7ed;
}
</style>

View File

@@ -26,6 +26,7 @@
<div class="flow-left"> <div class="flow-left">
<div class="flow-section-title"> <div class="flow-section-title">
<span>源卷列表</span> <span>源卷列表</span>
<schedule-detail-coil-bind />
<el-button v-if="!readonly" type="primary" size="small" @click="addSourceCoil">添加源卷</el-button> <el-button v-if="!readonly" type="primary" size="small" @click="addSourceCoil">添加源卷</el-button>
</div> </div>
@@ -374,6 +375,7 @@ import WarehouseSelect from "@/components/KLPService/WarehouseSelect";
import TimeInput from "@/components/TimeInput"; import TimeInput from "@/components/TimeInput";
import AbnormalForm from './components/AbnormalForm'; import AbnormalForm from './components/AbnormalForm';
import PlanSheetViewer from './components/PlanSheetViewer.vue'; import PlanSheetViewer from './components/PlanSheetViewer.vue';
import ScheduleDetailCoilBind from './components/ScheduleDetailCoilBind'
import { generateCoilNoPrefix } from "@/utils/coil/coilNo"; import { generateCoilNoPrefix } from "@/utils/coil/coilNo";
import ContractSelect from "@/components/KLPService/ContractSelect"; import ContractSelect from "@/components/KLPService/ContractSelect";
@@ -389,6 +391,7 @@ export default {
AbnormalForm, AbnormalForm,
ContractSelect, ContractSelect,
PlanSheetViewer, PlanSheetViewer,
ScheduleDetailCoilBind,
}, },
dicts: ['coil_quality_status', 'coil_abnormal_position', 'coil_abnormal_code', 'coil_abnormal_degree', 'coil_business_purpose'], dicts: ['coil_quality_status', 'coil_abnormal_position', 'coil_abnormal_code', 'coil_abnormal_degree', 'coil_business_purpose'],
data() { data() {

View File

@@ -11,6 +11,7 @@
:name="tab.value"></el-tab-pane> :name="tab.value"></el-tab-pane>
</el-tabs> </el-tabs>
<h3 class="section-title" v-else>待领物料列表</h3> <h3 class="section-title" v-else>待领物料列表</h3>
<schedule-detail-coil-bind />
<el-button size="mini" icon="el-icon-refresh" @click="getMaterialCoil">刷新</el-button> <el-button size="mini" icon="el-icon-refresh" @click="getMaterialCoil">刷新</el-button>
</div> </div>
@@ -309,6 +310,7 @@ import CoilCard from '@/components/KLPService/Renderer/CoilCard.vue'
import LabelRender from './LabelRender/index.vue' import LabelRender from './LabelRender/index.vue'
import StepSplit from './stepSplit.vue' import StepSplit from './stepSplit.vue'
import ExceptionManager from '../components/ExceptionManager' import ExceptionManager from '../components/ExceptionManager'
import ScheduleDetailCoilBind from '../components/ScheduleDetailCoilBind'
import { getCoilTagPrintType } from '@/views/wms/coil/js/coilPrint' import { getCoilTagPrintType } from '@/views/wms/coil/js/coilPrint'
export default { export default {
@@ -334,7 +336,8 @@ export default {
CoilCard, CoilCard,
LabelRender, LabelRender,
StepSplit, StepSplit,
ExceptionManager ExceptionManager,
ScheduleDetailCoilBind
}, },
data() { data() {
return { return {

View File

@@ -25,6 +25,7 @@
<div class="flow-left"> <div class="flow-left">
<div class="flow-section-title"> <div class="flow-section-title">
<span>母卷信息</span> <span>母卷信息</span>
<schedule-detail-coil-bind />
</div> </div>
<div class="coil-card mother-coil"> <div class="coil-card mother-coil">
<div class="coil-header"> <div class="coil-header">
@@ -377,6 +378,7 @@ import WarehouseSelect from "@/components/KLPService/WarehouseSelect";
import TimeInput from "@/components/TimeInput"; import TimeInput from "@/components/TimeInput";
import AbnormalForm from './components/AbnormalForm'; import AbnormalForm from './components/AbnormalForm';
import PlanSheetViewer from './components/PlanSheetViewer.vue'; import PlanSheetViewer from './components/PlanSheetViewer.vue';
import ScheduleDetailCoilBind from './components/ScheduleDetailCoilBind'
import { generateCoilNoPrefix } from "@/utils/coil/coilNo"; import { generateCoilNoPrefix } from "@/utils/coil/coilNo";
import ContractSelect from "@/components/KLPService/ContractSelect"; import ContractSelect from "@/components/KLPService/ContractSelect";
@@ -391,6 +393,7 @@ export default {
AbnormalForm, AbnormalForm,
ContractSelect, ContractSelect,
PlanSheetViewer, PlanSheetViewer,
ScheduleDetailCoilBind,
}, },
dicts: ['coil_quality_status', 'coil_abnormal_position', 'coil_abnormal_code', 'coil_abnormal_degree', 'coil_business_purpose'], dicts: ['coil_quality_status', 'coil_abnormal_position', 'coil_abnormal_code', 'coil_abnormal_degree', 'coil_business_purpose'],
data() { data() {

View File

@@ -33,6 +33,7 @@
<div slot="header" class="card-header"> <div slot="header" class="card-header">
<span><i class="el-icon-edit-outline"></i> {{ '更新信息' }}</span> <span><i class="el-icon-edit-outline"></i> {{ '更新信息' }}</span>
<div> <div>
<schedule-detail-coil-bind />
<el-button size="small" @click="saveTemp" :loading="loading">暂存内容</el-button> <el-button size="small" @click="saveTemp" :loading="loading">暂存内容</el-button>
<el-button type="primary" size="small" @click="handleSave" :loading="loading">保存更新</el-button> <el-button type="primary" size="small" @click="handleSave" :loading="loading">保存更新</el-button>
</div> </div>
@@ -405,6 +406,7 @@ import ContractSelect from "@/components/KLPService/ContractSelect";
import L2MatchPanel from './panels/L2MatchPanel.vue' import L2MatchPanel from './panels/L2MatchPanel.vue'
import DrMatchPanel from './panels/DrMatchPanel.vue'; import DrMatchPanel from './panels/DrMatchPanel.vue';
import PlanSheetViewer from './components/PlanSheetViewer.vue'; import PlanSheetViewer from './components/PlanSheetViewer.vue';
import ScheduleDetailCoilBind from './components/ScheduleDetailCoilBind'
// actionType -> 产线名称映射 // actionType -> 产线名称映射
@@ -431,6 +433,7 @@ export default {
L2MatchPanel, L2MatchPanel,
DrMatchPanel, DrMatchPanel,
PlanSheetViewer, PlanSheetViewer,
ScheduleDetailCoilBind,
}, },
dicts: ['coil_quality_status', 'coil_abnormal_position', 'coil_abnormal_code', 'coil_abnormal_degree', 'coil_business_purpose'], dicts: ['coil_quality_status', 'coil_abnormal_position', 'coil_abnormal_code', 'coil_abnormal_degree', 'coil_business_purpose'],
data() { data() {

View File

@@ -158,7 +158,7 @@
<div v-for="order in currentReq.orderList" :key="order.orderId" class="bound-order-card"> <div v-for="order in currentReq.orderList" :key="order.orderId" class="bound-order-card">
<div class="bound-order-card-header"> <div class="bound-order-card-header">
<span>{{ order.orderCode || ('订单 #' + order.orderId) }}</span> <span>{{ order.orderCode || ('订单 #' + order.orderId) }}</span>
<button v-if="canEdit" class="link-btn" @click="handleUnbindByOrderId(order.orderId)">解绑</button> <button v-if="canEdit" class="link-btn" @click="handleUnbindByOrderId(order)">解绑</button>
</div> </div>
<div class="bound-order-card-body"> <div class="bound-order-card-body">
<div style="font-size:12px; color:#7f8c8d; margin-bottom:3px;"> <div style="font-size:12px; color:#7f8c8d; margin-bottom:3px;">
@@ -399,6 +399,7 @@ import {
delRequirement, delRequirement,
addRel, addRel,
delRel, delRel,
listRel,
addRequirementDetail, addRequirementDetail,
updateRequirementDetail, updateRequirementDetail,
delRequirementDetail delRequirementDetail
@@ -628,7 +629,8 @@ export default {
searchBindOrders() { searchBindOrders() {
this.bindSearchLoading = true this.bindSearchLoading = true
listCrmOrder(this.bindQuery).then(res => { listCrmOrder(this.bindQuery).then(res => {
this.candidateOrderList = res.rows || [] const boundOrderIds = (this.currentReq?.orderList || []).map(o => o.orderId)
this.candidateOrderList = (res.rows || []).filter(o => !boundOrderIds.includes(o.orderId))
}).catch(() => { }).catch(() => {
this.candidateOrderList = [] this.candidateOrderList = []
}).finally(() => { }).finally(() => {
@@ -672,65 +674,87 @@ export default {
} }
this.bindBtnLoading = true this.bindBtnLoading = true
const scheduleId = this.currentReq.scheduleId const scheduleId = this.currentReq.scheduleId
const allPromises = []
this.selectedBindOrders.forEach(order => { const orderTasks = this.selectedBindOrders.map(order => {
// 1. 创建订单关联 return addRel({
allPromises.push(
addRel({
orderId: order.orderId, orderId: order.orderId,
scheduleId: scheduleId, scheduleId: scheduleId,
relWeight: null, relWeight: null,
remark: '' remark: ''
}) }).then(() => {
) if (!order.productContent) return
let products = []
// 2. 解析 productContent每行产品创建一条排产明细 let productType = ''
if (order.productContent) { try {
const parsed = parseProductContent(order.productContent) const parsed = parseProductContent(order.productContent)
const products = parsed.products || [] products = (parsed.products || []).filter(p => p.spec && p.material)
const productType = parsed.productName || '' productType = parsed.productName || ''
products.forEach(prod => { } catch (e) {
allPromises.push( return
}
if (products.length === 0) return
const detailPromises = products.map(prod =>
addRequirementDetail({ addRequirementDetail({
scheduleId: scheduleId, scheduleId: scheduleId,
orderDetailId: order.orderId, orderDetailId: order.orderId,
spec: prod.spec || '', spec: prod.spec,
material: prod.material || '', material: prod.material,
scheduleWeight: prod.quantity || 0, scheduleWeight: prod.quantity || 0,
productType: productType, productType: productType,
remark: prod.remark || '' remark: prod.remark || ''
}) })
) )
return Promise.all(detailPromises)
}) })
}
}) })
Promise.all(allPromises).then(() => { Promise.allSettled(orderTasks).then(results => {
this.$modal.msgSuccess('绑定成功,排产明细已生成') const successCount = results.filter(r => r.status === 'fulfilled').length
const failCount = results.filter(r => r.status === 'rejected').length
this.bindDialogVisible = false this.bindDialogVisible = false
this.handleReqClick(this.currentReq) this.handleReqClick(this.currentReq)
}).catch(() => { if (failCount === 0) {
this.$modal.msgError('绑定或创建明细失败') this.$modal.msgSuccess(`成功绑定 ${successCount} 个订单,排产明细已生成`)
} else if (successCount === 0) {
this.$modal.msgError('绑定失败,请重试')
} else {
this.$modal.msgWarning(`成功绑定 ${successCount} 个,${failCount} 个失败`)
}
}).finally(() => { }).finally(() => {
this.bindBtnLoading = false this.bindBtnLoading = false
}) })
}, },
handleUnbindByOrderId(orderId) { handleUnbindByOrderId(order) {
const order = (this.currentReq.orderList || []).find(o => o.orderId === orderId) this.$confirm('确认解绑该订单吗?解绑将同时删除对应的排产明细。', '提示', {
if (!order) {
this.$message.warning('未找到关联记录')
return
}
this.$confirm('确认解绑该订单吗?', '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
delRel(order.relId || orderId).then(() => { const tasks = []
// 1. 删除该订单关联的排产明细
const detailIds = this.requirementDetailList
.filter(d => d.orderDetailId === order.orderId)
.map(d => d.scheduleDetailId)
detailIds.forEach(id => {
tasks.push(delRequirementDetail(id))
})
// 2. 查询并删除关联表记录
tasks.push(
listRel({ orderId: order.orderId, scheduleId: this.currentReq.scheduleId }).then(res => {
const rows = res.rows || []
if (rows.length === 0) {
throw new Error('未找到关联记录')
}
const relId = rows[0].id || rows[0].relId
return delRel(relId)
})
)
Promise.all(tasks).then(() => {
this.$modal.msgSuccess('解绑成功') this.$modal.msgSuccess('解绑成功')
this.handleReqClick(this.currentReq) this.handleReqClick(this.currentReq)
}).catch(() => {
this.$modal.msgError('解绑失败')
}) })
}).catch(() => { }) }).catch(() => { })
}, },

View File

@@ -17,22 +17,22 @@
<el-tab-pane label="待审核" name="pending" /> <el-tab-pane label="待审核" name="pending" />
<el-tab-pane label="已排产" name="scheduled" /> <el-tab-pane label="已排产" name="scheduled" />
</el-tabs> </el-tabs>
<div class="aps-sch-summary" v-if="summaryText"> <div v-if="summaryText" class="aps-sch-summary">
<span>{{ summaryText }}</span> <span>{{ summaryText }}</span>
</div> </div>
</div> </div>
<!-- 待审核 Tab --> <!-- 待审核 Tab -->
<div class="detail-card aps-sch-card" v-show="activeTab === 'pending'"> <div v-show="activeTab === 'pending'" class="detail-card aps-sch-card">
<div class="detail-card-header"> <div class="detail-card-header">
<span>待审核产需单明细</span> <span>待审核产需单明细</span>
<span v-if="pendingScheduleList.length > 0" style="font-weight:normal;font-size:12px;opacity:0.8;"> <span v-if="pendingScheduleList.length > 0" style="font-weight:normal;font-size:12px;opacity:0.8;">
{{ pendingScheduleList.length }} 个产需单 {{ pendingScheduleList.length }} 个产需单
</span> </span>
</div> </div>
<div class="detail-card-body" style="padding:0;" v-loading="loading"> <div v-loading="loading" class="detail-card-body" style="padding:0;">
<div v-if="pendingScheduleList.length > 0" class="aps-sch-list"> <div v-if="pendingScheduleList.length > 0" class="aps-sch-list">
<div class="aps-sch-item" v-for="sch in pendingScheduleList" :key="sch.scheduleId"> <div v-for="sch in pendingScheduleList" :key="sch.scheduleId" class="aps-sch-item">
<!-- 产需单头部排产单号 + 状态标签 + 操作 --> <!-- 产需单头部排产单号 + 状态标签 + 操作 -->
<div class="aps-sch-item-header"> <div class="aps-sch-item-header">
<div class="aps-sch-item-header-left"> <div class="aps-sch-item-header-left">
@@ -131,7 +131,7 @@
</table> </table>
<!-- 明细列表 --> <!-- 明细列表 -->
<div class="aps-sch-item-details" v-if="(sch.detailList || []).length > 0"> <div v-if="(sch.detailList || []).length > 0" class="aps-sch-item-details">
<div class="aps-sch-detail-header"> <div class="aps-sch-detail-header">
<span class="aps-sch-detail-col col-spec">规格</span> <span class="aps-sch-detail-col col-spec">规格</span>
<span class="aps-sch-detail-col col-material">材质</span> <span class="aps-sch-detail-col col-material">材质</span>
@@ -140,9 +140,9 @@
<span class="aps-sch-detail-col col-remark">备注</span> <span class="aps-sch-detail-col col-remark">备注</span>
</div> </div>
<div <div
class="aps-sch-detail-row"
v-for="(d, di) in sch.detailList" v-for="(d, di) in sch.detailList"
:key="di" :key="di"
class="aps-sch-detail-row"
@click="handleDetailClick(sch, d)" @click="handleDetailClick(sch, d)"
> >
<span class="aps-sch-detail-col col-spec">{{ d.spec }}</span> <span class="aps-sch-detail-col col-spec">{{ d.spec }}</span>
@@ -162,34 +162,84 @@
</div> </div>
<!-- 已排产 Tab --> <!-- 已排产 Tab -->
<div class="detail-card aps-sch-card" v-show="activeTab === 'scheduled'"> <div v-show="activeTab === 'scheduled'" class="detail-card aps-sch-card">
<div class="detail-card-header"> <div class="detail-card-header">
<span>已排产明细</span> <span>已排产明细</span>
<div style="display:flex; align-items:center; gap:8px;">
<span v-if="scheduledItemList.length > 0" style="font-weight:normal;font-size:12px;opacity:0.8;"> <span v-if="scheduledItemList.length > 0" style="font-weight:normal;font-size:12px;opacity:0.8;">
{{ scheduledItemList.length }} {{ scheduledItemList.length }}
</span> </span>
<span v-if="selectedItems.length >= 2" style="margin-left: auto;">
<el-button
size="mini"
type="warning"
icon="el-icon-link"
@click.stop="handleMergePrepare"
>
合并选中项 ({{ selectedItems.length }})
</el-button>
</span>
<span v-else style="margin-left: auto;">
<el-button
size="mini"
type="warning"
icon="el-icon-link"
disabled
>
合并选中项
</el-button>
</span>
</div> </div>
<div class="detail-card-body" style="padding:0;" v-loading="schLoading"> </div>
<div v-loading="schLoading" class="detail-card-body" style="padding:0;">
<el-table <el-table
v-if="scheduledItemList.length > 0" v-if="scheduledItemList.length > 0"
ref="scheduledTable"
:data="scheduledItemList" :data="scheduledItemList"
border border
size="small" size="small"
class="aps-table" class="aps-table"
:row-class-name="getItemRowClassName"
@selection-change="handleSelectionChange"
> >
<el-table-column label="排产单号" prop="scheduleNo" min-width="140" /> <el-table-column type="selection" width="45" align="center" />
<el-table-column label="规格" prop="spec" min-width="120" /> <el-table-column label="排产单号" prop="scheduleNo" min-width="140" show-overflow-tooltip />
<el-table-column label="材质" prop="material" width="90" align="center" /> <el-table-column label="生产日期" prop="prodDate" width="110" align="center" show-overflow-tooltip />
<el-table-column label="排产吨数" prop="scheduleWeight" width="100" align="right" /> <el-table-column label="排产状态" prop="scheduleStatus" width="90" align="center">
<el-table-column label="品名" prop="productType" min-width="100" />
<el-table-column label="订货单位" prop="customerName" min-width="140" />
<el-table-column label="备注" prop="remark" min-width="100" />
<el-table-column label="操作" width="110" align="center" fixed="right">
<template slot-scope="scope"> <template slot-scope="scope">
<span style="white-space:nowrap;"> <span class="aps-status-tag" :class="'status-' + (scope.row.scheduleStatus || 1)">{{ statusMap[scope.row.scheduleStatus] || '未知' }}</span>
<el-button type="text" size="small" style="color:#409EFF;" @click="handleEditScheduled(scope.row)">编辑</el-button> </template>
<el-button type="text" size="small" style="color:#ff4d4f;" @click="handleDeleteScheduled(scope.row)">删除</el-button> </el-table-column>
</span> <el-table-column label="规格" prop="spec" min-width="120" show-overflow-tooltip />
<el-table-column label="材质" prop="material" width="90" align="center" show-overflow-tooltip />
<el-table-column label="排产吨数" prop="scheduleWeight" width="100" align="right" show-overflow-tooltip />
<el-table-column label="品名项" prop="productItem" min-width="100" show-overflow-tooltip />
<el-table-column label="品名" prop="productType" min-width="90" align="center" show-overflow-tooltip />
<el-table-column label="订货单位" prop="customerName" min-width="140" show-overflow-tooltip />
<el-table-column label="业务员" prop="businessUser" width="90" align="center" show-overflow-tooltip />
<el-table-column label="联系电话" prop="businessPhone" width="110" align="center" show-overflow-tooltip />
<el-table-column label="交货期(天)" prop="deliveryCycle" width="100" align="center" show-overflow-tooltip />
<el-table-column label="产品用途" prop="usePurpose" min-width="120" show-overflow-tooltip />
<el-table-column label="厚度公差" prop="thicknessTolerance" min-width="100" show-overflow-tooltip />
<el-table-column label="宽度公差" prop="widthTolerance" min-width="100" show-overflow-tooltip />
<el-table-column label="表面质量" prop="surfaceQuality" min-width="100" show-overflow-tooltip />
<el-table-column label="表面处理" prop="surfaceTreatment" min-width="100" show-overflow-tooltip />
<el-table-column label="内径尺寸" prop="innerDiameter" width="90" align="center" show-overflow-tooltip />
<el-table-column label="外径要求" prop="outerDiameter" width="90" align="center" show-overflow-tooltip />
<el-table-column label="包装要求" prop="packReq" min-width="100" show-overflow-tooltip />
<el-table-column label="切边要求" prop="cutEdgeReq" min-width="100" show-overflow-tooltip />
<el-table-column label="单件重量" prop="singleCoilWeight" width="90" align="center" show-overflow-tooltip />
<el-table-column label="交货重量偏差" prop="weightDeviation" min-width="110" show-overflow-tooltip />
<el-table-column label="其他技术要求" prop="otherTechReq" min-width="130" show-overflow-tooltip />
<el-table-column label="付款情况说明" prop="paymentDesc" min-width="130" show-overflow-tooltip />
<el-table-column label="单行排产备注" prop="rowRemark" min-width="100" show-overflow-tooltip />
<el-table-column label="备注" prop="remark" min-width="100" show-overflow-tooltip />
<el-table-column label="操作" width="150" align="center" fixed="right">
<template slot-scope="scope">
<div style="display:flex; justify-content:center; gap:0;">
<el-button type="text" size="small" style="color:#409EFF;padding:0 6px;" @click="handleEditScheduled(scope.row)">编辑</el-button>
<el-button type="text" size="small" style="color:#ff4d4f;padding:0 6px;" @click="handleDeleteScheduled(scope.row)">删除</el-button>
</div>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -237,33 +287,329 @@
</el-dialog> </el-dialog>
<!-- 已排产明细编辑对话框 --> <!-- 已排产明细编辑对话框 -->
<el-dialog :title="editDialogTitle" :visible.sync="editDialogVisible" width="500px" append-to-body <el-dialog
:close-on-click-modal="false"> :title="editDialogTitle"
<el-form ref="editForm" :model="editForm" label-width="100px" size="small"> :visible.sync="editDialogVisible"
width="800px"
append-to-body
:close-on-click-modal="false"
>
<el-form ref="editForm" :model="editForm" label-width="110px" size="small">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="排产单号"> <el-form-item label="排产单号">
<span style="color:#2c3e50;">{{ editForm.scheduleNo }}</span> <span style="color:#2c3e50;font-weight:500;">{{ editForm.scheduleNo }}</span>
</el-form-item> </el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="订货单位">
<el-input v-model="editForm.customerName" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="规格" prop="spec"> <el-form-item label="规格" prop="spec">
<el-input v-model="editForm.spec" /> <el-input v-model="editForm.spec" />
</el-form-item> </el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="材质" prop="material"> <el-form-item label="材质" prop="material">
<el-input v-model="editForm.material" /> <el-input v-model="editForm.material" />
</el-form-item> </el-form-item>
<el-form-item label="排产吨数" prop="scheduleWeight"> </el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="排产吨数">
<el-input-number v-model="editForm.scheduleWeight" :min="0" :precision="3" :controls="false" style="width:100%" /> <el-input-number v-model="editForm.scheduleWeight" :min="0" :precision="3" :controls="false" style="width:100%" />
</el-form-item> </el-form-item>
<el-form-item label="品名" prop="productType"> </el-col>
<el-col :span="12">
<el-form-item label="排产总计划吨数">
<el-input-number v-model="editForm.totalPlanWeight" :min="0" :precision="3" :controls="false" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="品名">
<el-input v-model="editForm.productType" /> <el-input v-model="editForm.productType" />
</el-form-item> </el-form-item>
<el-form-item label="备注" prop="remark"> </el-col>
<el-col :span="12">
<el-form-item label="品名项">
<el-input v-model="editForm.productItem" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="业务员">
<el-input v-model="editForm.businessUser" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话">
<el-input v-model="editForm.businessPhone" />
</el-form-item>
</el-col>
</el-row>
<!-- <el-row :gutter="16">-->
<!-- <el-col :span="12">-->
<!-- <el-form-item label="关联销售合同号">-->
<!-- <el-input v-model="editForm.relContractNo" />-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<!-- <el-col :span="12">-->
<!-- <el-form-item label="订单日期">-->
<!-- <el-date-picker v-model="editForm.orderDate" type="date" value-format="yyyy-MM-dd" style="width:100%" />-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<!-- </el-row>-->
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="交货期(天)">
<el-input-number v-model="editForm.deliveryCycle" :min="0" :controls="false" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="产品用途">
<el-input v-model="editForm.usePurpose" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="厚度公差">
<el-input v-model="editForm.thicknessTolerance" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="宽度公差">
<el-input v-model="editForm.widthTolerance" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="表面质量">
<el-input v-model="editForm.surfaceQuality" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="表面处理">
<el-input v-model="editForm.surfaceTreatment" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="内径尺寸">
<el-input v-model="editForm.innerDiameter" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="外径要求">
<el-input v-model="editForm.outerDiameter" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="包装要求">
<el-input v-model="editForm.packReq" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="切边要求">
<el-input v-model="editForm.cutEdgeReq" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="单件重量">
<el-input v-model="editForm.singleCoilWeight" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="交货重量偏差">
<el-input v-model="editForm.weightDeviation" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="其他技术要求">
<el-input v-model="editForm.otherTechReq" type="textarea" :rows="2" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="付款情况说明">
<el-input v-model="editForm.paymentDesc" type="textarea" :rows="2" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="单行排产备注">
<el-input v-model="editForm.rowRemark" type="textarea" :rows="2" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="备注">
<el-input v-model="editForm.remark" type="textarea" :rows="2" /> <el-input v-model="editForm.remark" type="textarea" :rows="2" />
</el-form-item> </el-form-item>
</el-col>
</el-row>
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button :loading="editBtnLoading" type="danger" @click="submitEditForm"> </el-button> <el-button :loading="editBtnLoading" type="danger" @click="submitEditForm"> </el-button>
<el-button @click="editDialogVisible = false"> </el-button> <el-button @click="editDialogVisible = false"> </el-button>
</div> </div>
</el-dialog> </el-dialog>
<!-- 合并排产明细对话框 -->
<el-dialog
title="合并排产明细"
:visible.sync="mergeDialogVisible"
width="850px"
append-to-body
:close-on-click-modal="false"
>
<div style="margin-bottom:12px;font-size:13px;color:#e67e22;">
选中 {{ mergeForm.itemCount }} 条明细请选择一个作为合并模板下方字段将自动填充您可修改后确认合并
</div>
<!-- 选择模板 -->
<el-table :data="mergeSourceRows" border size="small" style="margin-bottom:12px;" @row-click="pickMergeTemplate">
<el-table-column width="50" align="center">
<template slot-scope="scope">
<el-radio v-model="mergeTemplateIndex" :label="scope.$index" @click.native.stop />
</template>
</el-table-column>
<el-table-column label="排产单号" prop="scheduleNo" min-width="130" show-overflow-tooltip />
<el-table-column label="规格" prop="spec" min-width="100" show-overflow-tooltip />
<el-table-column label="材质" prop="material" width="80" show-overflow-tooltip />
<el-table-column label="排产吨数" prop="scheduleWeight" width="90" align="right" />
<el-table-column label="品名" prop="productType" min-width="80" show-overflow-tooltip />
<el-table-column label="订货单位" prop="customerName" min-width="120" show-overflow-tooltip />
</el-table>
<!-- 合并表单 -->
<el-form ref="mergeFormRef" :model="mergeForm" label-width="110px" size="small">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="排产单号"><el-input v-model="mergeForm.scheduleNo" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="订货单位"><el-input v-model="mergeForm.customerName" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="规格"><el-input v-model="mergeForm.spec" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="材质"><el-input v-model="mergeForm.material" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="排产吨数(累加)">
<el-input-number v-model="mergeForm.scheduleWeight" :min="0" :precision="3" :controls="false" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="品名"><el-input v-model="mergeForm.productType" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="品名项"><el-input v-model="mergeForm.productItem" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="业务员"><el-input v-model="mergeForm.businessUser" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="联系电话"><el-input v-model="mergeForm.businessPhone" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="交货期(天)">
<el-input-number v-model="mergeForm.deliveryCycle" :min="0" :controls="false" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="产品用途"><el-input v-model="mergeForm.usePurpose" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="厚度公差"><el-input v-model="mergeForm.thicknessTolerance" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="宽度公差"><el-input v-model="mergeForm.widthTolerance" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="表面质量"><el-input v-model="mergeForm.surfaceQuality" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="表面处理"><el-input v-model="mergeForm.surfaceTreatment" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="内径尺寸"><el-input v-model="mergeForm.innerDiameter" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="外径要求"><el-input v-model="mergeForm.outerDiameter" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="包装要求"><el-input v-model="mergeForm.packReq" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="切边要求"><el-input v-model="mergeForm.cutEdgeReq" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="单件重量"><el-input v-model="mergeForm.singleCoilWeight" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="交货重量偏差"><el-input v-model="mergeForm.weightDeviation" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="其他技术要求"><el-input v-model="mergeForm.otherTechReq" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="付款情况说明"><el-input v-model="mergeForm.paymentDesc" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="备注"><el-input v-model="mergeForm.remark" /></el-form-item>
</el-col>
</el-row>
</el-form>
<div style="margin-top:8px;font-size:12px;color:#909399;">
合并后 schedule_detail_ids{{ mergeForm.scheduleDetailIds }}
</div>
<div slot="footer" class="dialog-footer">
<el-button :loading="mergeBtnLoading" type="warning" @click="confirmMerge"> </el-button>
<el-button @click="mergeDialogVisible = false"> </el-button>
</div>
</el-dialog>
</div> </div>
</template> </template>
@@ -275,9 +621,10 @@ import {
import { import {
getCrmOrderInfo, getCrmOrderInfo,
listScheduleItem, listScheduleItem,
addScheduleItem,
updateScheduleItem, updateScheduleItem,
delScheduleItem delScheduleItem,
receiveScheduleItem,
mergeScheduleItem
} from '@/api/aps/schedule' } from '@/api/aps/schedule'
export default { export default {
@@ -301,6 +648,24 @@ export default {
// 已排产 // 已排产
scheduledItemList: [], scheduledItemList: [],
selectedItems: [],
sourceColorMap: {},
// 合并对话框
mergeDialogVisible: false,
mergeBtnLoading: false,
mergeTemplateIndex: 0,
mergeSourceRows: [],
mergeForm: {
itemCount: 0, scheduleNo: '', customerName: '', spec: '', material: '',
scheduleWeight: 0, productType: '', productItem: '', businessUser: '',
businessPhone: '', deliveryCycle: undefined, usePurpose: '',
thicknessTolerance: '', widthTolerance: '', surfaceQuality: '',
surfaceTreatment: '', innerDiameter: '', outerDiameter: '',
packReq: '', cutEdgeReq: '', singleCoilWeight: '',
weightDeviation: '', otherTechReq: '', paymentDesc: '',
remark: '', scheduleDetailIds: ''
},
// 驳回 // 驳回
rejectDialogVisible: false, rejectDialogVisible: false,
@@ -322,20 +687,47 @@ export default {
drillOrder: null drillOrder: null
} }
}, },
watch: {
activeTab() {
this.selectedItems = []
}
},
created() { created() {
this.handleQuery() this.handleQuery()
}, },
methods: { methods: {
getEmptyEditForm() { getEmptyEditForm() {
return { return {
itemId: undefined,
scheduleId: undefined, scheduleId: undefined,
scheduleNo: '', scheduleNo: '',
prodDate: '',
scheduleStatus: undefined,
totalPlanWeight: undefined,
relContractNo: '',
businessUser: '',
businessPhone: '',
orderDate: '',
customerName: '',
deliveryCycle: undefined,
usePurpose: '',
productType: '',
thicknessTolerance: '',
widthTolerance: '',
surfaceQuality: '',
surfaceTreatment: '',
innerDiameter: '',
outerDiameter: '',
packReq: '',
cutEdgeReq: '',
singleCoilWeight: '',
weightDeviation: '',
otherTechReq: '',
paymentDesc: '',
spec: '', spec: '',
material: '', material: '',
scheduleWeight: 0, scheduleWeight: 0,
productType: '', productItem: '',
customerName: '', rowRemark: '',
remark: '' remark: ''
} }
}, },
@@ -386,40 +778,18 @@ export default {
}, },
handleAccept(sch) { handleAccept(sch) {
this.$confirm(
`确认接收产需单「${sch.scheduleNo}」的全部排产明细吗?`,
'提示',
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
).then(() => {
const details = sch.detailList || [] const details = sch.detailList || []
if (details.length === 0) { if (details.length === 0) {
this.$message.warning('该产需单无可排产的明细') this.$message.warning('该产需单无可排产的明细')
return return
} }
this.$confirm(
const promises = details.map(detail => `确认接收产需单「${sch.scheduleNo}」的全部排产明细吗?共 ${details.length} 条明细`,
addScheduleItem({ '提示',
scheduleId: sch.scheduleId, { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
scheduleNo: sch.scheduleNo, ).then(() => {
spec: detail.spec || '',
material: detail.material || '',
scheduleWeight: detail.scheduleWeight || 0,
productType: sch.productType || detail.productType || '',
customerName: sch.customerName || '',
remark: detail.remark || ''
})
)
// 更新产需单状态为 2已接收
promises.push(
updateRequirement({
scheduleId: sch.scheduleId,
scheduleStatus: 2
})
)
this.$message.info('正在处理接收...') this.$message.info('正在处理接收...')
Promise.all(promises).then(() => { receiveScheduleItem(sch.scheduleId).then(() => {
this.$modal.msgSuccess('接收成功,排产明细已写入') this.$modal.msgSuccess('接收成功,排产明细已写入')
this.queryPending() this.queryPending()
}).catch(() => { }).catch(() => {
@@ -463,12 +833,24 @@ export default {
queryScheduled() { queryScheduled() {
this.schLoading = true this.schLoading = true
this.scheduledItemList = [] this.scheduledItemList = []
this.sourceColorMap = {}
this.selectedItems = []
listScheduleItem({ prodDate: this.queryDate, pageNum: 1, pageSize: 999 }).then(res => { listScheduleItem({ prodDate: this.queryDate, pageNum: 1, pageSize: 999 }).then(res => {
this.scheduledItemList = res.rows || [] this.scheduledItemList = (res.rows || []).sort((a, b) => (a.scheduleNo || '').localeCompare(b.scheduleNo || ''))
const totalWeight = this.scheduledItemList.reduce((sum, d) => sum + (parseFloat(d.scheduleWeight) || 0), 0) try {
this.sourceColorMap = this.buildGroupColorMap(this.scheduledItemList)
} catch (e) {
console.error('buildGroupColorMap error:', e)
this.sourceColorMap = {}
}
const totalWeight = this.scheduledItemList.reduce((sum, d) => {
const w = parseFloat(d.scheduleWeight)
return sum + (isNaN(w) ? 0 : w)
}, 0)
this.summaryText = `${this.scheduledItemList.length} 条明细,排产总吨数 ${totalWeight.toFixed(3)}` this.summaryText = `${this.scheduledItemList.length} 条明细,排产总吨数 ${totalWeight.toFixed(3)}`
}).catch(() => { }).catch((e) => {
console.error('queryScheduled error:', e)
this.scheduledItemList = [] this.scheduledItemList = []
this.summaryText = '' this.summaryText = ''
}).finally(() => { }).finally(() => {
@@ -504,7 +886,7 @@ export default {
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
delScheduleItem(row.itemId || row.scheduleItemId).then(() => { delScheduleItem(row.scheduleId).then(() => {
this.$modal.msgSuccess('删除成功') this.$modal.msgSuccess('删除成功')
this.queryScheduled() this.queryScheduled()
}).catch(() => { }).catch(() => {
@@ -513,6 +895,97 @@ export default {
}).catch(() => {}) }).catch(() => {})
}, },
// ====== 合并 ======
handleSelectionChange(rows) {
this.selectedItems = rows
},
getItemRowClassName({ row }) {
const key = row.scheduleNo
if (!key) return ''
const colorIndex = this.sourceColorMap[key]
return colorIndex !== undefined ? `merge-source-${colorIndex}` : ''
},
buildGroupColorMap(list) {
const map = {}
let colorIdx = 0
list.forEach(item => {
const key = item.scheduleNo
if (!key) return
if (!(key in map)) {
map[key] = colorIdx % 5
colorIdx++
}
})
return map
},
handleMergePrepare() {
if (this.selectedItems.length < 2) {
this.$message.warning('请至少选择2条排产明细进行合并')
return
}
this.mergeSourceRows = [...this.selectedItems]
this.mergeTemplateIndex = 0
this.pickMergeTemplate(this.mergeSourceRows[0])
this.mergeDialogVisible = true
},
pickMergeTemplate(row) {
const idx = this.mergeSourceRows.indexOf(row)
if (idx >= 0) this.mergeTemplateIndex = idx
this.mergeForm = {
itemCount: this.mergeSourceRows.length,
scheduleNo: row.scheduleNo || '',
customerName: row.customerName || '',
spec: row.spec || '',
material: row.material || '',
scheduleWeight: this.mergeSourceRows.reduce((sum, r) => sum + (parseFloat(r.scheduleWeight) || 0), 0),
productType: row.productType || '',
productItem: row.productItem || '',
businessUser: row.businessUser || '',
businessPhone: row.businessPhone || '',
deliveryCycle: row.deliveryCycle,
usePurpose: row.usePurpose || '',
thicknessTolerance: row.thicknessTolerance || '',
widthTolerance: row.widthTolerance || '',
surfaceQuality: row.surfaceQuality || '',
surfaceTreatment: row.surfaceTreatment || '',
innerDiameter: row.innerDiameter || '',
outerDiameter: row.outerDiameter || '',
packReq: row.packReq || '',
cutEdgeReq: row.cutEdgeReq || '',
singleCoilWeight: row.singleCoilWeight || '',
weightDeviation: row.weightDeviation || '',
otherTechReq: row.otherTechReq || '',
paymentDesc: row.paymentDesc || '',
remark: row.remark || '',
scheduleDetailIds: this.mergeSourceRows.map(r => r.scheduleDetailIds || '').filter(Boolean).join(',')
}
},
confirmMerge() {
this.mergeBtnLoading = true
const ids = this.mergeSourceRows.map(r => r.scheduleId)
const mergedBo = {
...this.mergeForm,
prodDate: this.queryDate,
scheduleStatus: 2
}
delete mergedBo.itemCount
mergeScheduleItem({ ids, mergedBo }).then(() => {
this.$modal.msgSuccess(`合并成功:${ids.length} 条明细合并为 1 条`)
this.mergeDialogVisible = false
this.selectedItems = []
this.queryScheduled()
}).catch(() => {
this.$modal.msgError('合并失败')
}).finally(() => {
this.mergeBtnLoading = false
})
},
// ====== 下钻 & 辅助方法 ====== // ====== 下钻 & 辅助方法 ======
scheduleTotalWeight(sch) { scheduleTotalWeight(sch) {
const details = sch.detailList || [] const details = sch.detailList || []
@@ -594,6 +1067,7 @@ export default {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
min-height: 0; min-height: 0;
overflow-x: auto;
} }
} }
@@ -830,4 +1304,21 @@ export default {
color: $aps-text-muted; color: $aps-text-muted;
font-size: 12px; font-size: 12px;
} }
// ====== 合并候选行颜色高亮(同一源排产单 → 同色) ======
::v-deep .el-table__body tr.merge-source-0 > td {
background-color: #f0f5ff !important;
}
::v-deep .el-table__body tr.merge-source-1 > td {
background-color: #f6ffed !important;
}
::v-deep .el-table__body tr.merge-source-2 > td {
background-color: #fff7e6 !important;
}
::v-deep .el-table__body tr.merge-source-3 > td {
background-color: #f9f0ff !important;
}
::v-deep .el-table__body tr.merge-source-4 > td {
background-color: #fff0f6 !important;
}
</style> </style>