feat: 实现产需单按工序步骤生成排产明细功能

1. 重构接收产需单接口,支持按配置工序步骤生成明细
2. 新增工艺、工艺步骤CRUD接口与管理页面
3. 新增工序选择组件
4. 优化产需单页面,增加历史记录功能
5. 为排产明细添加工序步骤名称展示
This commit is contained in:
2026-07-04 15:43:05 +08:00
parent ce09ac9da3
commit 2fde9ec993
12 changed files with 1529 additions and 427 deletions

View File

@@ -2,6 +2,8 @@ package com.klp.flow.domain.bo;
import lombok.Data; import lombok.Data;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import javax.validation.constraints.NotEmpty;
import java.util.List;
/** /**
* 产需单接收请求对象 * 产需单接收请求对象
@@ -18,4 +20,55 @@ public class SchProdScheduleItemReceiveBo {
@NotNull(message = "排产单主表ID不能为空") @NotNull(message = "排产单主表ID不能为空")
private Long scheduleId; private Long scheduleId;
/**
* 明细工序配置列表
*/
@NotEmpty(message = "明细工序配置不能为空")
private List<DetailProcessConfig> detailProcessList;
/**
* 明细工序配置
*/
@Data
public static class DetailProcessConfig {
/**
* 产需单明细ID
*/
@NotNull(message = "明细ID不能为空")
private Long scheduleDetailId;
/**
* 工序ID
*/
@NotNull(message = "工序ID不能为空")
private Long processId;
/**
* 工序步骤列表
*/
@NotEmpty(message = "工序步骤不能为空")
private List<StepConfig> stepList;
}
/**
* 工序步骤配置
*/
@Data
public static class StepConfig {
/**
* 步骤ID
*/
@NotNull(message = "步骤ID不能为空")
private Long stepId;
/**
* 步骤顺序号
*/
private Long stepOrder;
/**
* 步骤名称
*/
private String stepName;
}
} }

View File

@@ -2,10 +2,13 @@ package com.klp.flow.domain.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.annotation.ExcelProperty;
import com.baomidou.mybatisplus.annotation.TableField;
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 java.util.List;
/** /**
* 工序定义主视图对象 sch_prod_process * 工序定义主视图对象 sch_prod_process
@@ -44,5 +47,10 @@ public class SchProdProcessVo {
@ExcelProperty(value = "备注") @ExcelProperty(value = "备注")
private String remark; private String remark;
/**
* 工艺步骤列表
*/
@TableField(exist = false)
private List<SchProdProcessStepVo> stepList;
} }

View File

@@ -2,6 +2,7 @@ package com.klp.flow.domain.vo;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.annotation.ExcelProperty;
@@ -195,6 +196,12 @@ public class SchProdScheduleItemVo {
@ExcelProperty(value = "工序ID") @ExcelProperty(value = "工序ID")
private Long actionId; private Long actionId;
/**
* 工序步骤名称
*/
@TableField(exist = false)
private String stepName;
/** /**
* 规格 例1.0X1250 * 规格 例1.0X1250
*/ */

View File

@@ -11,13 +11,18 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.klp.flow.domain.bo.SchProdProcessBo; import com.klp.flow.domain.bo.SchProdProcessBo;
import com.klp.flow.domain.vo.SchProdProcessVo; import com.klp.flow.domain.vo.SchProdProcessVo;
import com.klp.flow.domain.vo.SchProdProcessStepVo;
import com.klp.flow.domain.SchProdProcess; import com.klp.flow.domain.SchProdProcess;
import com.klp.flow.domain.SchProdProcessStep;
import com.klp.flow.mapper.SchProdProcessMapper; import com.klp.flow.mapper.SchProdProcessMapper;
import com.klp.flow.mapper.SchProdProcessStepMapper;
import com.klp.flow.service.ISchProdProcessService; import com.klp.flow.service.ISchProdProcessService;
import java.util.Collections;
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业务层处理
@@ -30,13 +35,18 @@ import java.util.Collection;
public class SchProdProcessServiceImpl implements ISchProdProcessService { public class SchProdProcessServiceImpl implements ISchProdProcessService {
private final SchProdProcessMapper baseMapper; private final SchProdProcessMapper baseMapper;
private final SchProdProcessStepMapper stepMapper;
/** /**
* 查询工序定义主 * 查询工序定义主
*/ */
@Override @Override
public SchProdProcessVo queryById(Long processId){ public SchProdProcessVo queryById(Long processId){
return baseMapper.selectVoById(processId); SchProdProcessVo vo = baseMapper.selectVoById(processId);
if (vo != null) {
fillStepList(Collections.singletonList(vo));
}
return vo;
} }
/** /**
@@ -46,6 +56,9 @@ public class SchProdProcessServiceImpl implements ISchProdProcessService {
public TableDataInfo<SchProdProcessVo> queryPageList(SchProdProcessBo bo, PageQuery pageQuery) { public TableDataInfo<SchProdProcessVo> queryPageList(SchProdProcessBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<SchProdProcess> lqw = buildQueryWrapper(bo); LambdaQueryWrapper<SchProdProcess> lqw = buildQueryWrapper(bo);
Page<SchProdProcessVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw); Page<SchProdProcessVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
if (result.getRecords() != null && !result.getRecords().isEmpty()) {
fillStepList(result.getRecords());
}
return TableDataInfo.build(result); return TableDataInfo.build(result);
} }
@@ -55,7 +68,34 @@ public class SchProdProcessServiceImpl implements ISchProdProcessService {
@Override @Override
public List<SchProdProcessVo> queryList(SchProdProcessBo bo) { public List<SchProdProcessVo> queryList(SchProdProcessBo bo) {
LambdaQueryWrapper<SchProdProcess> lqw = buildQueryWrapper(bo); LambdaQueryWrapper<SchProdProcess> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw); List<SchProdProcessVo> list = baseMapper.selectVoList(lqw);
if (list != null && !list.isEmpty()) {
fillStepList(list);
}
return list;
}
/**
* 批量填充工艺步骤列表
*/
private void fillStepList(List<SchProdProcessVo> voList) {
List<Long> processIds = voList.stream()
.map(SchProdProcessVo::getProcessId)
.filter(id -> id != null)
.distinct()
.collect(Collectors.toList());
if (processIds.isEmpty()) {
return;
}
LambdaQueryWrapper<SchProdProcessStep> stepQw = Wrappers.lambdaQuery();
stepQw.in(SchProdProcessStep::getProcessId, processIds);
stepQw.orderByAsc(SchProdProcessStep::getStepOrder);
List<SchProdProcessStepVo> allSteps = stepMapper.selectVoList(stepQw);
Map<Long, List<SchProdProcessStepVo>> stepMap = allSteps.stream()
.collect(Collectors.groupingBy(SchProdProcessStepVo::getProcessId));
for (SchProdProcessVo vo : voList) {
vo.setStepList(stepMap.getOrDefault(vo.getProcessId(), Collections.emptyList()));
}
} }
private LambdaQueryWrapper<SchProdProcess> buildQueryWrapper(SchProdProcessBo bo) { private LambdaQueryWrapper<SchProdProcess> buildQueryWrapper(SchProdProcessBo bo) {

View File

@@ -18,15 +18,18 @@ 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.SchProdSchedule;
import com.klp.flow.domain.SchProdScheduleDetail; import com.klp.flow.domain.SchProdScheduleDetail;
import com.klp.flow.domain.SchProdProcessStep;
import com.klp.flow.mapper.SchProdScheduleItemMapper; import com.klp.flow.mapper.SchProdScheduleItemMapper;
import com.klp.flow.mapper.SchProdScheduleMapper; import com.klp.flow.mapper.SchProdScheduleMapper;
import com.klp.flow.mapper.SchProdScheduleDetailMapper; import com.klp.flow.mapper.SchProdScheduleDetailMapper;
import com.klp.flow.mapper.SchProdProcessStepMapper;
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.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -46,6 +49,7 @@ public class SchProdScheduleItemServiceImpl implements ISchProdScheduleItemServi
private final SchProdScheduleItemMapper baseMapper; private final SchProdScheduleItemMapper baseMapper;
private final SchProdScheduleMapper schProdScheduleMapper; private final SchProdScheduleMapper schProdScheduleMapper;
private final SchProdScheduleDetailMapper schProdScheduleDetailMapper; private final SchProdScheduleDetailMapper schProdScheduleDetailMapper;
private final SchProdProcessStepMapper stepMapper;
/** /**
* 查询排产单主加明细可合并 * 查询排产单主加明细可合并
@@ -62,6 +66,9 @@ public class SchProdScheduleItemServiceImpl implements ISchProdScheduleItemServi
public TableDataInfo<SchProdScheduleItemVo> queryPageList(SchProdScheduleItemBo bo, PageQuery pageQuery) { public TableDataInfo<SchProdScheduleItemVo> queryPageList(SchProdScheduleItemBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<SchProdScheduleItem> lqw = buildQueryWrapper(bo); LambdaQueryWrapper<SchProdScheduleItem> lqw = buildQueryWrapper(bo);
Page<SchProdScheduleItemVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw); Page<SchProdScheduleItemVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
if (result.getRecords() != null && !result.getRecords().isEmpty()) {
fillStepName(result.getRecords());
}
return TableDataInfo.build(result); return TableDataInfo.build(result);
} }
@@ -71,7 +78,11 @@ public class SchProdScheduleItemServiceImpl implements ISchProdScheduleItemServi
@Override @Override
public List<SchProdScheduleItemVo> queryList(SchProdScheduleItemBo bo) { public List<SchProdScheduleItemVo> queryList(SchProdScheduleItemBo bo) {
LambdaQueryWrapper<SchProdScheduleItem> lqw = buildQueryWrapper(bo); LambdaQueryWrapper<SchProdScheduleItem> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw); List<SchProdScheduleItemVo> list = baseMapper.selectVoList(lqw);
if (list != null && !list.isEmpty()) {
fillStepName(list);
}
return list;
} }
private LambdaQueryWrapper<SchProdScheduleItem> buildQueryWrapper(SchProdScheduleItemBo bo) { private LambdaQueryWrapper<SchProdScheduleItem> buildQueryWrapper(SchProdScheduleItemBo bo) {
@@ -113,6 +124,30 @@ public class SchProdScheduleItemServiceImpl implements ISchProdScheduleItemServi
return lqw; return lqw;
} }
/**
* 批量填充工序步骤名称
*/
private void fillStepName(List<SchProdScheduleItemVo> voList) {
List<Long> stepIds = voList.stream()
.map(SchProdScheduleItemVo::getActionId)
.filter(id -> id != null)
.distinct()
.collect(Collectors.toList());
if (stepIds.isEmpty()) {
return;
}
LambdaQueryWrapper<SchProdProcessStep> stepQw = Wrappers.lambdaQuery();
stepQw.in(SchProdProcessStep::getStepId, stepIds);
List<SchProdProcessStep> steps = stepMapper.selectList(stepQw);
Map<Long, String> stepNameMap = steps.stream()
.collect(Collectors.toMap(SchProdProcessStep::getStepId, SchProdProcessStep::getStepName, (v1, v2) -> v1));
for (SchProdScheduleItemVo vo : voList) {
if (vo.getActionId() != null) {
vo.setStepName(stepNameMap.get(vo.getActionId()));
}
}
}
/** /**
* 新增排产单主加明细可合并 * 新增排产单主加明细可合并
*/ */
@@ -195,12 +230,13 @@ public class SchProdScheduleItemServiceImpl implements ISchProdScheduleItemServi
} }
/** /**
* 接收产需单:从 sch_prod_schedule + sch_prod_schedule_detail 全字段复制到 sch_prod_schedule_item * 接收产需单:根据配置的工序步骤生成多条排产单明细
*/ */
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public Boolean receiveByBo(SchProdScheduleItemReceiveBo receiveBo) { public Boolean receiveByBo(SchProdScheduleItemReceiveBo receiveBo) {
Long scheduleId = receiveBo.getScheduleId(); Long scheduleId = receiveBo.getScheduleId();
List<SchProdScheduleItemReceiveBo.DetailProcessConfig> detailProcessList = receiveBo.getDetailProcessList();
// 1. 查询产需单主表 // 1. 查询产需单主表
SchProdSchedule header = schProdScheduleMapper.selectById(scheduleId); SchProdSchedule header = schProdScheduleMapper.selectById(scheduleId);
@@ -217,57 +253,74 @@ public class SchProdScheduleItemServiceImpl implements ISchProdScheduleItemServi
throw new RuntimeException("产需单无明细数据scheduleId=" + scheduleId); throw new RuntimeException("产需单无明细数据scheduleId=" + scheduleId);
} }
// 3. 遍历每条 detail构建 SchProdScheduleItem 列表 // 3. 构建明细ID到工序配置的映射
List<SchProdScheduleItem> addList = new ArrayList<>(details.size()); Map<Long, SchProdScheduleItemReceiveBo.DetailProcessConfig> processConfigMap = new HashMap<>();
for (SchProdScheduleDetail detail : details) { for (SchProdScheduleItemReceiveBo.DetailProcessConfig config : detailProcessList) {
SchProdScheduleItem item = new SchProdScheduleItem(); processConfigMap.put(config.getScheduleDetailId(), config);
// 从 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. 批量插入 // 4. 遍历每条 detail根据工序步骤生成多条 SchProdScheduleItem
List<SchProdScheduleItem> addList = new ArrayList<>();
for (SchProdScheduleDetail detail : details) {
SchProdScheduleItemReceiveBo.DetailProcessConfig processConfig = processConfigMap.get(detail.getScheduleDetailId());
if (processConfig == null || processConfig.getStepList() == null || processConfig.getStepList().isEmpty()) {
throw new RuntimeException("明细ID=" + detail.getScheduleDetailId() + " 未配置工序步骤");
}
// 根据工序步骤数量生成多条排产单明细
for (SchProdScheduleItemReceiveBo.StepConfig step : processConfig.getStepList()) {
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为actionId
item.setActionId(step.getStepId());
// 来源追溯未合并各存明细ID
item.setScheduleDetailIds(String.valueOf(detail.getScheduleDetailId()));
validEntityBeforeSave(item);
addList.add(item);
}
}
// 5. 批量插入
baseMapper.insertBatch(addList); baseMapper.insertBatch(addList);
// 5. 更新产需单状态为 2已下达 // 6. 更新产需单状态为 2已下达
header.setScheduleStatus(2L); header.setScheduleStatus(2L);
schProdScheduleMapper.updateById(header); schProdScheduleMapper.updateById(header);

View File

@@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询工艺列表
export function listProcess(query) {
return request({
url: '/flow/prodProcess/list',
method: 'get',
params: query
})
}
// 查询工艺详细
export function getProcess(processId) {
return request({
url: '/flow/prodProcess/' + processId,
method: 'get'
})
}
// 新增工艺
export function addProcess(data) {
return request({
url: '/flow/prodProcess',
method: 'post',
data: data
})
}
// 修改工艺
export function updateProcess(data) {
return request({
url: '/flow/prodProcess',
method: 'put',
data: data
})
}
// 删除工艺
export function delProcess(processIds) {
return request({
url: '/flow/prodProcess/' + processIds,
method: 'delete'
})
}

View File

@@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询工艺步骤列表
export function listProcessStep(query) {
return request({
url: '/flow/prodProcessStep/list',
method: 'get',
params: query
})
}
// 查询工艺步骤详细
export function getProcessStep(stepId) {
return request({
url: '/flow/prodProcessStep/' + stepId,
method: 'get'
})
}
// 新增工艺步骤
export function addProcessStep(data) {
return request({
url: '/flow/prodProcessStep',
method: 'post',
data: data
})
}
// 修改工艺步骤
export function updateProcessStep(data) {
return request({
url: '/flow/prodProcessStep',
method: 'put',
data: data
})
}
// 删除工艺步骤
export function delProcessStep(stepIds) {
return request({
url: '/flow/prodProcessStep/' + stepIds,
method: 'delete'
})
}

View File

@@ -73,12 +73,12 @@ export function delScheduleItem(ids) {
// ====== 排产单明细项 接收/合并 ====== // ====== 排产单明细项 接收/合并 ======
// 接收产需单(后端全字段复制 // 接收产需单(根据配置的工序步骤生成排产明细
export function receiveScheduleItem(scheduleId) { export function receiveScheduleItem(data) {
return request({ return request({
url: '/flow/prodScheduleItem/receive', url: '/flow/prodScheduleItem/receive',
method: 'post', method: 'post',
data: { scheduleId } data: data
}) })
} }

View File

@@ -0,0 +1,112 @@
<template>
<div class="process-select">
<el-select
v-model="processId"
:placeholder="placeholder"
:clearable="clearable"
:disabled="disabled"
filterable
class="process-select-input"
@clear="handleClear"
>
<el-option
v-for="item in processList"
:key="item.processId"
:label="item.processName"
:value="item.processId"
>
<span class="process-option-name">{{ item.processName }}</span>
<span v-if="item._steps" class="process-option-steps">{{ item._steps }}</span>
</el-option>
</el-select>
</div>
</template>
<script>
import { listProcess } from '@/api/aps/process'
export default {
name: 'ProcessSelect',
props: {
value: {
type: [String, Number, undefined],
default: undefined
},
placeholder: {
type: String,
default: '请选择工序'
},
clearable: {
type: Boolean,
default: true
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
processList: []
}
},
computed: {
processId: {
get() {
return this.value
},
set(val) {
const process = this.processList.find(item => item.processId === val) || null
this.$emit('input', val)
this.$emit('change', val, process)
}
}
},
created() {
this.getProcessList()
},
methods: {
getProcessList() {
listProcess({ pageNum: 1, pageSize: 1000 }).then(res => {
const rows = res.rows || []
this.processList = rows.map(item => ({
...item,
_steps: (item.stepList && item.stepList.length > 0)
? item.stepList.map(s => s.stepName).join(' → ')
: ''
}))
}).catch(() => {
this.processList = []
})
},
handleClear() {
this.$emit('input', undefined)
this.$emit('change', undefined, null)
}
}
}
</script>
<style scoped lang="scss">
.process-select {
width: 100%;
}
.process-select-input {
width: 100%;
}
.process-option-name {
font-size: 14px;
font-weight: 500;
color: #303133;
}
.process-option-steps {
display: block;
margin-top: 4px;
font-size: 11px;
color: #909399;
line-height: 1.4;
}
</style>

View File

@@ -0,0 +1,589 @@
<template>
<div class="app-container" style="height: calc(100vh - 84px); display: flex;">
<!-- 左侧工艺列表 -->
<div class="left-panel" v-loading="processLoading"
style="width: 35%; border-right: 1px solid #e4e7ed; overflow-y: auto;">
<!-- 筛选区 -->
<div class="filter-section" style="padding: 10px; border-bottom: 1px solid #e4e7ed;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
<div style="display: flex; align-items: center; gap: 4px; flex-wrap: wrap;">
<el-input v-model="queryParams.processName" placeholder="工艺名称" clearable @keyup.enter.native="handleSearch"
style="width: 160px;" />
<el-button class="aps-btn-red" icon="el-icon-search" size="mini" @click="handleSearch">筛选</el-button>
<el-button icon="el-icon-refresh" size="mini" class="aps-btn-silver" @click="resetQuery">重置</el-button>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<el-button class="aps-btn-red" icon="el-icon-plus" size="mini" @click="handleAddProcess">新增工艺</el-button>
<span style="font-size: 12px; color: #909399;">
<span class="aps-total-count">{{ total }}</span>
</span>
</div>
</div>
</div>
<!-- 列表区域 -->
<div class="custom-list">
<div class="list-body">
<div v-for="item in processList" :key="item.processId" class="list-item"
:class="{ 'list-item-active': currentProcess && currentProcess.processId === item.processId }"
@click="handleProcessClick(item)">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<div style="font-weight: bold; font-size: 14px;">{{ item.processName }}</div>
<div style="display: flex; gap: 4px;">
<el-button type="text" icon="el-icon-edit" size="mini" @click.stop="handleUpdateProcess(item)"></el-button>
<el-button type="text" icon="el-icon-delete" size="mini" style="color: #F56C6C;" @click.stop="handleDeleteProcess(item)"></el-button>
</div>
</div>
<div style="font-size: 12px; color: #909399; margin-bottom: 4px;">
{{ item.processDesc || '暂无描述' }}
</div>
<div style="font-size: 12px; color: #909399;">
备注: {{ item.remark || '-' }}
</div>
</div>
<div v-if="processList.length === 0 && !processLoading"
style="padding: 40px; text-align: center; color: #909399;">
暂无工艺数据
</div>
</div>
</div>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="getProcessList" style="padding: 10px; margin-bottom: 10px !important;" />
</div>
<!-- 右侧工艺步骤区域 -->
<div class="right-panel" v-if="currentProcess && currentProcess.processId"
style="flex: 1; display: flex; flex-direction: column;" v-loading="stepLoading">
<div class="detail-panel">
<!-- 工艺基本信息卡片 -->
<div class="detail-card">
<div class="detail-card-header">
<span>工艺基本信息</span>
</div>
<div class="detail-card-body">
<div class="form-grid-2">
<div class="form-field"><label>工艺名称</label>
<div class="field-value">{{ currentProcess.processName }}</div>
</div>
<div class="form-field"><label>工艺描述</label>
<div class="field-value">{{ currentProcess.processDesc || '-' }}</div>
</div>
<div class="form-field" style="grid-column:1/3;"><label>备注</label>
<div class="field-value">{{ currentProcess.remark || '-' }}</div>
</div>
</div>
</div>
</div>
<!-- 工艺步骤卡片 -->
<div class="detail-card">
<div class="detail-card-header">
<span>工艺步骤{{ stepList.length }} </span>
<el-button type="text" icon="el-icon-plus" size="mini" style="color: white;" @click="handleAddStep">新增步骤</el-button>
</div>
<div class="detail-card-body">
<div v-if="stepList.length > 0" class="process-timeline-wrap">
<el-timeline>
<el-timeline-item
v-for="(step, index) in stepList"
:key="step.stepId"
:timestamp="'步骤 ' + step.stepOrder"
placement="top"
:type="index === stepList.length - 1 ? 'success' : 'primary'"
:hollow="index !== stepList.length - 1"
size="large"
>
<div class="timeline-content">
<div class="timeline-header">
<span class="step-name">{{ step.stepName }}</span>
<div class="step-actions">
<el-button type="text" icon="el-icon-edit" size="mini" @click="handleUpdateStep(step)"></el-button>
<el-button type="text" icon="el-icon-delete" size="mini" style="color: #F56C6C;" @click="handleDeleteStep(step)"></el-button>
</div>
</div>
<div v-if="step.remark" class="step-remark">{{ step.remark }}</div>
</div>
</el-timeline-item>
</el-timeline>
</div>
<el-empty v-else description="暂无工艺步骤" />
</div>
</div>
</div>
</div>
<div v-else style="flex: 1; display: flex; flex-direction: column;">
<el-empty description="选择工艺后查看步骤" />
</div>
<!-- 新增/编辑工艺弹窗 -->
<el-dialog :title="processTitle" :visible.sync="processOpen" width="500px" append-to-body>
<el-form ref="processForm" :model="processForm" :rules="processRules" label-width="80px">
<el-form-item label="工艺名称" prop="processName">
<el-input v-model="processForm.processName" placeholder="请输入工艺名称" />
</el-form-item>
<el-form-item label="工艺描述" prop="processDesc">
<el-input v-model="processForm.processDesc" type="textarea" :rows="3" placeholder="请输入工艺描述" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="processForm.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="submitLoading" @click="submitProcessForm"> </el-button>
<el-button @click="cancelProcess"> </el-button>
</div>
</el-dialog>
<!-- 新增/编辑工艺步骤弹窗 -->
<el-dialog :title="stepTitle" :visible.sync="stepOpen" width="500px" append-to-body>
<el-form ref="stepForm" :model="stepForm" :rules="stepRules" label-width="80px">
<el-form-item label="步骤顺序" prop="stepOrder">
<el-input-number v-model="stepForm.stepOrder" :min="1" :max="99" controls-position="right" />
</el-form-item>
<el-form-item label="步骤名称" prop="stepName">
<el-input v-model="stepForm.stepName" placeholder="请输入步骤名称" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="stepForm.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="submitLoading" @click="submitStepForm"> </el-button>
<el-button @click="cancelStep"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listProcess, getProcess, addProcess, updateProcess, delProcess } from '@/api/aps/process'
import { listProcessStep, addProcessStep, updateProcessStep, delProcessStep } from '@/api/aps/processStep'
export default {
name: 'ApsProcesses',
data() {
return {
// 工艺相关
processLoading: false,
processList: [],
total: 0,
currentProcess: null,
queryParams: {
processName: '',
pageNum: 1,
pageSize: 10
},
processTitle: '',
processOpen: false,
processForm: {},
processRules: {
processName: [
{ required: true, message: '工艺名称不能为空', trigger: 'blur' }
]
},
// 工艺步骤相关
stepLoading: false,
submitLoading: false,
stepList: [],
stepTitle: '',
stepOpen: false,
stepForm: {},
stepRules: {
stepOrder: [
{ required: true, message: '步骤顺序不能为空', trigger: 'blur' }
],
stepName: [
{ required: true, message: '步骤名称不能为空', trigger: 'blur' }
]
}
}
},
created() {
this.getProcessList()
},
methods: {
/** 查询工艺列表 */
getProcessList() {
this.processLoading = true
listProcess(this.queryParams).then(res => {
this.processList = res.rows || []
this.total = res.total || 0
}).catch(() => {
this.processList = []
this.total = 0
}).finally(() => {
this.processLoading = false
})
},
/** 搜索 */
handleSearch() {
this.queryParams.pageNum = 1
this.getProcessList()
},
/** 重置 */
resetQuery() {
this.queryParams.processName = ''
this.handleSearch()
},
/** 点击工艺 */
handleProcessClick(process) {
this.currentProcess = process
this.getStepList(process.processId)
},
/** 查询工艺步骤列表 */
getStepList(processId) {
this.stepLoading = true
listProcessStep({ processId }).then(res => {
this.stepList = res.rows || []
}).catch(() => {
this.stepList = []
}).finally(() => {
this.stepLoading = false
})
},
/** 新增工艺 */
handleAddProcess() {
this.resetProcessForm()
this.processTitle = '新增工艺'
this.processOpen = true
},
/** 修改工艺 */
handleUpdateProcess(row) {
this.resetProcessForm()
const processId = row.processId || this.currentProcess.processId
getProcess(processId).then(res => {
this.processForm = res.data
this.processTitle = '修改工艺'
this.processOpen = true
})
},
/** 提交工艺表单 */
submitProcessForm() {
this.$refs['processForm'].validate(valid => {
if (valid) {
this.submitLoading = true
if (this.processForm.processId != undefined) {
updateProcess(this.processForm).then(response => {
this.$modal.msgSuccess('修改成功')
this.processOpen = false
this.getProcessList()
}).finally(() => {
this.submitLoading = false
})
} else {
addProcess(this.processForm).then(response => {
this.$modal.msgSuccess('新增成功')
this.processOpen = false
this.getProcessList()
}).finally(() => {
this.submitLoading = false
})
}
}
})
},
/** 删除工艺 */
handleDeleteProcess(row) {
const processIds = row.processId || this.currentProcess.processId
this.$modal.confirm('是否确认删除该工艺?').then(() => {
return delProcess(processIds)
}).then(() => {
this.getProcessList()
this.$modal.msgSuccess('删除成功')
this.currentProcess = null
this.stepList = []
}).catch(() => {})
},
/** 取消工艺弹窗 */
cancelProcess() {
this.processOpen = false
this.resetProcessForm()
},
/** 重置工艺表单 */
resetProcessForm() {
this.processForm = {
processId: undefined,
processName: undefined,
processDesc: undefined,
remark: undefined
}
this.resetForm('processForm')
},
/** 新增步骤 */
handleAddStep() {
this.resetStepForm()
this.stepForm.processId = this.currentProcess.processId
// 自动计算下一步骤顺序
if (this.stepList.length > 0) {
const maxOrder = Math.max(...this.stepList.map(s => s.stepOrder || 0))
this.stepForm.stepOrder = maxOrder + 1
} else {
this.stepForm.stepOrder = 1
}
this.stepTitle = '新增工艺步骤'
this.stepOpen = true
},
/** 修改步骤 */
handleUpdateStep(row) {
this.resetStepForm()
this.stepForm = { ...row }
this.stepTitle = '修改工艺步骤'
this.stepOpen = true
},
/** 提交步骤表单 */
submitStepForm() {
this.$refs['stepForm'].validate(valid => {
if (valid) {
this.submitLoading = true
if (this.stepForm.stepId != undefined) {
updateProcessStep(this.stepForm).then(response => {
this.$modal.msgSuccess('修改成功')
this.stepOpen = false
this.getStepList(this.currentProcess.processId)
}).finally(() => {
this.submitLoading = false
})
} else {
addProcessStep(this.stepForm).then(response => {
this.$modal.msgSuccess('新增成功')
this.stepOpen = false
this.getStepList(this.currentProcess.processId)
}).finally(() => {
this.submitLoading = false
})
}
}
})
},
/** 删除步骤 */
handleDeleteStep(row) {
const stepIds = row.stepId
this.$modal.confirm('是否确认删除该工艺步骤?').then(() => {
return delProcessStep(stepIds)
}).then(() => {
this.getStepList(this.currentProcess.processId)
this.$modal.msgSuccess('删除成功')
}).catch(() => {})
},
/** 取消步骤弹窗 */
cancelStep() {
this.stepOpen = false
this.resetStepForm()
},
/** 重置步骤表单 */
resetStepForm() {
this.stepForm = {
stepId: undefined,
processId: undefined,
stepOrder: undefined,
stepName: undefined,
remark: undefined
}
this.resetForm('stepForm')
}
}
}
</script>
<style scoped lang="scss">
@import './scss/aps-theme.scss';
.app-container {
overflow: hidden;
padding: 0;
}
.left-panel {
height: calc(100vh - 84px);
box-sizing: border-box;
overflow-y: auto;
}
.right-panel {
height: calc(100vh - 84px);
overflow: hidden;
min-height: 0;
}
.custom-list {
border: 1px solid #e4e7ed;
border-radius: 4px;
overflow: hidden;
}
.list-item {
padding: 12px;
border-bottom: 2px solid #dddddd;
cursor: pointer;
transition: background-color 0.2s;
}
.list-item:hover {
background-color: $aps-silver-1;
}
.list-item-active {
background-color: $aps-red-1;
border-left: 3px solid $aps-red-2;
}
.aps-total-count {
color: $aps-red-2;
font-weight: bold;
}
.aps-btn-red {
@include aps-btn-red;
}
.aps-btn-silver {
@include aps-btn-silver;
}
.detail-panel {
flex: 1;
overflow-y: scroll;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
background: $aps-bg;
min-height: 0;
}
.detail-card {
background: $aps-white;
border: 1px solid $aps-border;
border-radius: $aps-radius;
box-shadow: $aps-shadow-sm;
}
.detail-card-header {
background: linear-gradient(to right, $aps-red-2, $aps-red-3);
color: white;
padding: 8px 14px;
font-size: 13px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: space-between;
}
.detail-card-body {
padding: 14px;
}
.form-grid-2 {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px 16px;
}
.form-field {
display: flex;
flex-direction: column;
gap: 3px;
}
.form-field label {
font-size: 11px;
color: $aps-text-muted;
font-weight: 500;
}
.form-field .field-value {
font-size: 13px;
color: $aps-text;
padding: 4px 0;
border-bottom: 1px solid $aps-silver-mid;
}
.aps-product-table {
width: 100%;
::v-deep th {
background: $aps-silver-1 !important;
color: $aps-silver-5 !important;
font-weight: 600 !important;
}
}
// ====== 工艺步骤时间线 ======
.process-timeline-wrap {
padding: 20px 10px;
::v-deep .el-timeline {
padding-left: 10px;
}
::v-deep .el-timeline-item__wrapper {
padding-left: 20px;
}
::v-deep .el-timeline-item__timestamp {
font-size: 12px;
font-weight: 600;
color: $aps-text-muted;
}
::v-deep .el-timeline-item__tail {
border-left-color: $aps-silver-3;
}
::v-deep .el-timeline-item__node--primary {
background-color: $aps-red-2;
border-color: $aps-red-2;
}
::v-deep .el-timeline-item__node--success {
background-color: #67C23A;
border-color: #67C23A;
}
}
.timeline-content {
background: $aps-white;
border: 1px solid $aps-border;
border-radius: $aps-radius;
padding: 12px 16px;
transition: box-shadow 0.2s, border-color 0.2s;
&:hover {
border-color: $aps-red-2;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
}
.timeline-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.step-name {
font-size: 14px;
font-weight: 600;
color: $aps-text;
}
.step-actions {
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.2s;
.timeline-content:hover & {
opacity: 1;
}
}
.step-remark {
margin-top: 8px;
font-size: 12px;
color: $aps-text-muted;
padding-top: 8px;
border-top: 1px dashed $aps-silver-3;
}
</style>

View File

@@ -7,7 +7,7 @@
<div class="filter-section" style="padding: 10px; border-bottom: 1px solid #e4e7ed;"> <div class="filter-section" style="padding: 10px; border-bottom: 1px solid #e4e7ed;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;"> <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
<div style="display: flex; align-items: center; gap: 4px; flex-wrap: wrap;"> <div style="display: flex; align-items: center; gap: 4px; flex-wrap: wrap;">
<el-input v-model="queryParams.scheduleNo" placeholder="产单号" clearable @keyup.enter.native="handleSearch" <el-input v-model="queryParams.scheduleNo" placeholder="产单号" clearable @keyup.enter.native="handleSearch"
style="width: 130px;" size="small" /> style="width: 130px;" size="small" />
<el-input v-model="queryParams.customerName" placeholder="客户名" clearable <el-input v-model="queryParams.customerName" placeholder="客户名" clearable
@keyup.enter.native="handleSearch" style="width: 130px;" size="small" /> @keyup.enter.native="handleSearch" style="width: 130px;" size="small" />
@@ -58,15 +58,19 @@
<div class="detail-card-header"> <div class="detail-card-header">
<span>产需单信息</span> <span>产需单信息</span>
<div style="display:flex; gap:6px;"> <div style="display:flex; gap:6px;">
<button class="header-btn" :disabled="currentReq.scheduleStatus !== 0 && currentReq.scheduleStatus !== 3" @click="handleEdit(currentReq)">编辑</button> <button class="header-btn"
<button v-if="currentReq.scheduleStatus === 0 || currentReq.scheduleStatus === 3" class="header-btn" style="background:rgba(255,255,255,0.4);border-color:rgba(255,255,255,0.7);" @click="handleDispatch">{{ currentReq.scheduleStatus === 3 ? '重新下发' : '下发' }}</button> :disabled="currentReq.scheduleStatus !== 0 && currentReq.scheduleStatus !== 3"
@click="handleEdit(currentReq)">编辑</button>
<button v-if="currentReq.scheduleStatus === 0 || currentReq.scheduleStatus === 3" class="header-btn"
style="background:rgba(255,255,255,0.4);border-color:rgba(255,255,255,0.7);"
@click="handleDispatch">{{ currentReq.scheduleStatus === 3 ? '重新下发' : '下发' }}</button>
</div> </div>
</div> </div>
<div class="detail-card-body"> <div class="detail-card-body">
<table class="req-info-table"> <table class="req-info-table">
<tbody> <tbody>
<tr> <tr>
<td class="req-td-label">产单号</td> <td class="req-td-label">单号</td>
<td class="req-td-value">{{ currentReq.scheduleNo }}</td> <td class="req-td-value">{{ currentReq.scheduleNo }}</td>
<td class="req-td-label">生产日期</td> <td class="req-td-label">生产日期</td>
<td class="req-td-value">{{ currentReq.prodDate }}</td> <td class="req-td-value">{{ currentReq.prodDate }}</td>
@@ -75,7 +79,7 @@
<td class="req-td-label">排产状态</td> <td class="req-td-label">排产状态</td>
<td class="req-td-value"><span class="aps-status-tag" <td class="req-td-value"><span class="aps-status-tag"
:class="'status-' + (currentReq.scheduleStatus || 0)">{{ statusMap[currentReq.scheduleStatus] || :class="'status-' + (currentReq.scheduleStatus || 0)">{{ statusMap[currentReq.scheduleStatus] ||
'未知' }}</span></td> '未知' }}</span></td>
<td class="req-td-label">业务员</td> <td class="req-td-label">业务员</td>
<td class="req-td-value">{{ currentReq.businessUser }}</td> <td class="req-td-value">{{ currentReq.businessUser }}</td>
</tr> </tr>
@@ -139,7 +143,8 @@
</tr> </tr>
<tr v-if="currentReq.returnReason"> <tr v-if="currentReq.returnReason">
<td class="req-td-label" style="color:#e74c3c;">驳回原因</td> <td class="req-td-label" style="color:#e74c3c;">驳回原因</td>
<td class="req-td-value" colspan="3" style="color:#e74c3c;background:#fdecea;">{{ currentReq.returnReason }}</td> <td class="req-td-value" colspan="3" style="color:#e74c3c;background:#fdecea;">{{
currentReq.returnReason }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -190,8 +195,10 @@
<el-table-column label="操作" width="130" align="center" fixed="right"> <el-table-column label="操作" width="130" align="center" fixed="right">
<template slot-scope="scope"> <template slot-scope="scope">
<span style="white-space:nowrap;"> <span style="white-space:nowrap;">
<el-button v-if="canEdit" type="text" size="small" style="color:#409EFF;" @click="handleDetailEdit(scope.row)">编辑</el-button> <el-button v-if="canEdit" type="text" size="small" style="color:#409EFF;"
<el-button v-if="canEdit" type="text" size="small" style="color:#ff4d4f;" @click="handleDetailDelete(scope.row)">删除</el-button> @click="handleDetailEdit(scope.row)">编辑</el-button>
<el-button v-if="canEdit" type="text" size="small" style="color:#ff4d4f;"
@click="handleDetailDelete(scope.row)">删除</el-button>
</span> </span>
</template> </template>
</el-table-column> </el-table-column>
@@ -204,13 +211,15 @@
</el-row> </el-row>
<!-- 新增/编辑对话框 --> <!-- 新增/编辑对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="700px" append-to-body <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="900px" append-to-body
:close-on-click-modal="false"> :close-on-click-modal="false">
<el-form ref="reqForm" :model="reqForm" :rules="reqRules" label-width="110px" size="small"> <div style="display: flex; gap: 16px;">
<!-- 第一行必填项 --> <div style="flex: 1; min-width: 0;">
<el-form ref="reqForm" :model="reqForm" :rules="reqRules" label-width="80px" size="small">
<!-- 第一行必填项 -->
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="产单号" prop="scheduleNo"> <el-form-item label="产单号" prop="scheduleNo">
<el-input v-model="reqForm.scheduleNo" placeholder="自动生成或手动填写" /> <el-input v-model="reqForm.scheduleNo" placeholder="自动生成或手动填写" />
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -282,19 +291,42 @@
<el-form-item label="交货重量偏差" prop="weightDeviation"> <el-form-item label="交货重量偏差" prop="weightDeviation">
<el-input v-model="reqForm.weightDeviation" /> <el-input v-model="reqForm.weightDeviation" />
</el-form-item> </el-form-item>
<el-form-item label="其他技术要求" prop="otherTechReq">
<el-input v-model="reqForm.otherTechReq" type="textarea" :rows="2" />
</el-form-item>
<el-form-item label="付款情况说明" prop="paymentDesc"> <el-form-item label="付款情况说明" prop="paymentDesc">
<el-input v-model="reqForm.paymentDesc" type="textarea" :rows="2" /> <el-input v-model="reqForm.paymentDesc" type="textarea" :rows="2" />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row>
<el-col :span="24">
<el-form-item label="其他技术要求" prop="otherTechReq">
<el-input v-model="reqForm.otherTechReq" type="textarea" :rows="2" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark"> <el-form-item label="备注" prop="remark">
<el-input v-model="reqForm.remark" type="textarea" :rows="2" /> <el-input v-model="reqForm.remark" type="textarea" :rows="2" />
</el-form-item> </el-form-item>
</el-form> </el-form>
</div>
<!-- 右侧历史记录 -->
<div style="width: 200px; flex-shrink: 0;" v-if="!reqForm.scheduleId && scheduleNoHistory.length > 0">
<div style="font-size: 13px; font-weight: 600; color: #2c3e50; margin-bottom: 8px; padding-left: 8px; border-left: 3px solid #ff4d4f;">
历史记录
</div>
<div style="border: 1px solid #e4e7ed; border-radius: 4px; max-height: 450px; overflow-y: auto;">
<div v-for="item in scheduleNoHistory" :key="item.scheduleNo" class="history-item"
@click="useHistoryRecord(item)"
style="padding: 8px 10px; border-bottom: 1px solid #ecf0f1; cursor: pointer; font-size: 12px; transition: background 0.15s;">
<div style="font-weight: 500; color: #2c3e50;">{{ item.scheduleNo }}</div>
<div style="color: #7f8c8d; margin-top: 2px;">{{ item.customerName || '-' }}</div>
<div style="color: #b0b0b0; font-size: 11px;">{{ item.prodDate || '-' }}</div>
</div>
</div>
</div>
</div>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button :loading="btnLoading" type="danger" @click="submitForm"> </el-button> <el-button :loading="btnLoading" type="danger" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button> <el-button @click="dialogVisible = false"> </el-button>
@@ -376,7 +408,8 @@
<el-input v-model="detailForm.material" placeholder="请输入材质" /> <el-input v-model="detailForm.material" placeholder="请输入材质" />
</el-form-item> </el-form-item>
<el-form-item label="排产吨数" prop="scheduleWeight"> <el-form-item label="排产吨数" prop="scheduleWeight">
<el-input-number v-model="detailForm.scheduleWeight" :min="0" :precision="3" :controls="false" style="width:100%" /> <el-input-number v-model="detailForm.scheduleWeight" :min="0" :precision="3" :controls="false"
style="width:100%" />
</el-form-item> </el-form-item>
<el-form-item label="备注" prop="remark"> <el-form-item label="备注" prop="remark">
<el-input v-model="detailForm.remark" type="textarea" :rows="2" placeholder="可选" /> <el-input v-model="detailForm.remark" type="textarea" :rows="2" placeholder="可选" />
@@ -458,9 +491,10 @@ export default {
dialogVisible: false, dialogVisible: false,
dialogTitle: '新增产需单', dialogTitle: '新增产需单',
btnLoading: false, btnLoading: false,
scheduleNoHistory: [],
reqForm: this.getEmptyForm(), reqForm: this.getEmptyForm(),
reqRules: { reqRules: {
scheduleNo: [{ required: true, message: '产单号不能为空', trigger: 'blur' }], scheduleNo: [{ required: true, message: '产单号不能为空', trigger: 'blur' }],
prodDate: [{ required: true, message: '生产日期不能为空', trigger: 'change' }] prodDate: [{ required: true, message: '生产日期不能为空', trigger: 'change' }]
} }
} }
@@ -564,6 +598,17 @@ export default {
handleAdd() { handleAdd() {
this.resetForm() this.resetForm()
this.reqForm.scheduleNo = this.generateScheduleNo()
this.reqForm.prodDate = this.getTodayStr()
const lastForm = this.getLastFormData()
if (lastForm) {
Object.keys(lastForm).forEach(key => {
if (key !== 'scheduleId' && key !== 'scheduleNo' && key !== 'prodDate' && lastForm[key] !== undefined && lastForm[key] !== '' && lastForm[key] !== null) {
this.reqForm[key] = lastForm[key]
}
})
}
this.scheduleNoHistory = this.getScheduleNoHistory()
this.dialogTitle = '新增产需单' this.dialogTitle = '新增产需单'
this.dialogVisible = true this.dialogVisible = true
this.$nextTick(() => { this.$nextTick(() => {
@@ -586,9 +631,14 @@ export default {
this.$refs.reqForm.validate(valid => { this.$refs.reqForm.validate(valid => {
if (!valid) return if (!valid) return
this.btnLoading = true this.btnLoading = true
const action = this.reqForm.scheduleId ? updateRequirement : addRequirement const isNew = !this.reqForm.scheduleId
const action = isNew ? addRequirement : updateRequirement
action(this.reqForm).then(() => { action(this.reqForm).then(() => {
this.$modal.msgSuccess(this.reqForm.scheduleId ? '修改成功' : '新增成功') this.$modal.msgSuccess(isNew ? '新增成功' : '修改成功')
if (isNew) {
this.saveHistoryRecord(this.reqForm)
this.saveLastFormData(this.reqForm)
}
this.dialogVisible = false this.dialogVisible = false
this.getList() this.getList()
}).catch(() => { }).catch(() => {
@@ -615,6 +665,71 @@ export default {
}).catch(() => { }) }).catch(() => { })
}, },
getTodayStr() {
return new Date().toISOString().split('T')[0]
},
generateScheduleNo() {
const now = new Date()
const dateStr = now.getFullYear().toString() +
(now.getMonth() + 1).toString().padStart(2, '0') +
now.getDate().toString().padStart(2, '0')
const prefix = 'PCXD' + dateStr
const existingNums = this.reqList
.filter(item => item.scheduleNo && item.scheduleNo.startsWith(prefix))
.map(item => {
const seq = parseInt(item.scheduleNo.substring(prefix.length), 10)
return isNaN(seq) ? 0 : seq
})
const maxSeq = existingNums.length > 0 ? Math.max(...existingNums) : 0
const newSeq = (maxSeq + 1).toString().padStart(3, '0')
return prefix + newSeq
},
getScheduleNoHistory() {
try {
return JSON.parse(localStorage.getItem('aps_requirement_history') || '[]')
} catch (e) {
return []
}
},
saveHistoryRecord(formData) {
if (!formData.scheduleNo) return
const history = this.getScheduleNoHistory()
const idx = history.findIndex(item => item.scheduleNo === formData.scheduleNo)
if (idx >= 0) history.splice(idx, 1)
history.unshift({ ...formData })
delete history[0].scheduleId
if (history.length > 20) history.pop()
localStorage.setItem('aps_requirement_history', JSON.stringify(history))
},
useHistoryRecord(item) {
Object.keys(item).forEach(key => {
if (key !== 'scheduleId' && item[key] !== undefined) {
this.reqForm[key] = item[key]
}
})
this.$nextTick(() => {
this.$refs.reqForm && this.$refs.reqForm.clearValidate()
})
},
getLastFormData() {
try {
return JSON.parse(localStorage.getItem('aps_requirement_last_form') || 'null')
} catch (e) {
return null
}
},
saveLastFormData(formData) {
const data = { ...formData }
delete data.scheduleId
localStorage.setItem('aps_requirement_last_form', JSON.stringify(data))
},
openBindDialog() { openBindDialog() {
this.bindDialogVisible = true this.bindDialogVisible = true
this.selectedBindOrders = [] this.selectedBindOrders = []
@@ -1223,4 +1338,8 @@ export default {
.aps-bind-dialog ::v-deep .el-dialog__body { .aps-bind-dialog ::v-deep .el-dialog__body {
padding: 16px 20px 0 20px; padding: 16px 20px 0 20px;
} }
.history-item:hover {
background: #fdecea;
}
</style> </style>

View File

@@ -15,6 +15,7 @@
<el-button size="small" class="aps-btn-red" icon="el-icon-search" @click="handleQuery">查询</el-button> <el-button size="small" class="aps-btn-red" icon="el-icon-search" @click="handleQuery">查询</el-button>
<el-tabs v-model="activeTab" size="small" style="margin:0 0 0 16px;" @tab-click="handleQuery"> <el-tabs v-model="activeTab" size="small" style="margin:0 0 0 16px;" @tab-click="handleQuery">
<el-tab-pane label="待审核" name="pending" /> <el-tab-pane label="待审核" name="pending" />
<el-tab-pane label="已接收" name="accepted" />
<el-tab-pane label="已排产" name="scheduled" /> <el-tab-pane label="已排产" name="scheduled" />
</el-tabs> </el-tabs>
<div v-if="summaryText" class="aps-sch-summary"> <div v-if="summaryText" class="aps-sch-summary">
@@ -161,6 +162,104 @@
</div> </div>
</div> </div>
<!-- 已接收 Tab -->
<div v-loading="acceptedLoading" v-show="activeTab === 'accepted'" class="detail-card aps-sch-card">
<div class="detail-card-header">
<span>已接收产需单</span>
<span v-if="acceptedScheduleList.length > 0" style="font-weight:normal;font-size:12px;opacity:0.8;">
{{ acceptedScheduleList.length }} 个产需单
</span>
</div>
<div class="detail-card-body" style="padding:0;">
<div v-if="acceptedScheduleList.length > 0" class="aps-sch-list">
<div v-for="sch in acceptedScheduleList" :key="sch.scheduleId" class="aps-sch-item">
<div class="aps-sch-item-header">
<div class="aps-sch-item-header-left">
<span class="aps-sch-no">{{ sch.scheduleNo }}</span>
<span class="aps-status-tag status-2">已接收</span>
<span class="aps-sch-header-extra">
{{ sch.customerName }} · {{ sch.businessUser }} · {{ sch.deliveryCycle }}
</span>
</div>
</div>
<table class="req-info-table">
<tbody>
<tr>
<td class="req-td-label">排产单号</td><td class="req-td-value">{{ sch.scheduleNo }}</td>
<td class="req-td-label">生产日期</td><td class="req-td-value">{{ sch.prodDate }}</td>
</tr>
<tr>
<td class="req-td-label">业务员</td><td class="req-td-value">{{ sch.businessUser }}</td>
<td class="req-td-label">联系电话</td><td class="req-td-value">{{ sch.businessPhone }}</td>
</tr>
<tr>
<td class="req-td-label">订货单位</td><td class="req-td-value">{{ sch.customerName }}</td>
<td class="req-td-label">交货期()</td><td class="req-td-value">{{ sch.deliveryCycle }}</td>
</tr>
<tr>
<td class="req-td-label">品名</td><td class="req-td-value" colspan="3">{{ sch.productType }}</td>
</tr>
<tr>
<td class="req-td-label">产品用途</td><td class="req-td-value" colspan="3">{{ sch.usePurpose }}</td>
</tr>
<tr>
<td class="req-td-label">厚度公差</td><td class="req-td-value">{{ sch.thicknessTolerance }}</td>
<td class="req-td-label">宽度公差</td><td class="req-td-value">{{ sch.widthTolerance }}</td>
</tr>
<tr>
<td class="req-td-label">表面质量</td><td class="req-td-value">{{ sch.surfaceQuality }}</td>
<td class="req-td-label">表面处理</td><td class="req-td-value">{{ sch.surfaceTreatment }}</td>
</tr>
<tr>
<td class="req-td-label">内径尺寸</td><td class="req-td-value">{{ sch.innerDiameter }}</td>
<td class="req-td-label">外径要求</td><td class="req-td-value">{{ sch.outerDiameter }}</td>
</tr>
<tr>
<td class="req-td-label">包装要求</td><td class="req-td-value">{{ sch.packReq }}</td>
<td class="req-td-label">切边要求</td><td class="req-td-value">{{ sch.cutEdgeReq }}</td>
</tr>
<tr>
<td class="req-td-label">单件重量</td><td class="req-td-value">{{ sch.singleCoilWeight }}</td>
<td class="req-td-label">交货重量偏差</td><td class="req-td-value">{{ sch.weightDeviation }}</td>
</tr>
<tr v-if="sch.otherTechReq">
<td class="req-td-label">其他技术要求</td><td class="req-td-value" colspan="3">{{ sch.otherTechReq }}</td>
</tr>
<tr v-if="sch.paymentDesc">
<td class="req-td-label">付款情况说明</td><td class="req-td-value" colspan="3">{{ sch.paymentDesc }}</td>
</tr>
<tr v-if="sch.remark">
<td class="req-td-label">备注</td><td class="req-td-value" colspan="3">{{ sch.remark }}</td>
</tr>
</tbody>
</table>
<div v-if="(sch.detailList || []).length > 0" class="aps-sch-item-details">
<div class="aps-sch-detail-header">
<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-weight">排产吨数</span>
<span class="aps-sch-detail-col col-type">品名</span>
<span class="aps-sch-detail-col col-remark">备注</span>
</div>
<div v-for="(d, di) in sch.detailList" :key="di" class="aps-sch-detail-row" @click="handleDetailClick(sch, d)">
<span class="aps-sch-detail-col col-spec">{{ d.spec }}</span>
<span class="aps-sch-detail-col col-material">{{ d.material }}</span>
<span class="aps-sch-detail-col col-weight">{{ d.scheduleWeight }}</span>
<span class="aps-sch-detail-col col-type">{{ sch.productType || d.productType || '' }}</span>
<span class="aps-sch-detail-col col-remark">{{ d.remark }}</span>
</div>
</div>
<div v-else class="aps-sch-item-empty">暂无明细</div>
</div>
</div>
<div v-else-if="!acceptedLoading" style="padding:40px;text-align:center;color:#909399;">
{{ hasQueried ? '该日期暂无已接收产需单' : '请选择日期查询' }}
</div>
</div>
</div>
<!-- 已排产 Tab --> <!-- 已排产 Tab -->
<div v-loading="schLoading" v-show="activeTab === 'scheduled'" class="detail-card aps-sch-card"> <div v-loading="schLoading" v-show="activeTab === 'scheduled'" class="detail-card aps-sch-card">
<div class="detail-card-header"> <div class="detail-card-header">
@@ -169,85 +268,73 @@
<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> </div>
<div element-loading-text="正在加载已排产数据..." class="detail-card-body" style="padding:0;"> <div element-loading-text="正在加载已排产数据..." class="detail-card-body" style="padding:0;">
<el-table <template v-if="scheduledItemList.length > 0">
v-if="scheduledItemList.length > 0" <!-- 步骤工序 Tab -->
ref="scheduledTable" <div class="aps-step-tabs">
:data="scheduledItemList" <span
border v-for="tab in scheduledStepTabs"
size="small" :key="tab"
class="aps-table" class="aps-step-tab"
:row-class-name="getItemRowClassName" :class="{ 'aps-step-tab-active': scheduledStepTab === tab }"
@selection-change="handleSelectionChange" @click="scheduledStepTab = tab"
> >
<el-table-column type="selection" width="45" align="center" /> {{ tab }}
<el-table-column label="排产单号" prop="scheduleNo" min-width="140" show-overflow-tooltip /> <span class="aps-step-tab-count">{{ scheduledItemList.filter(i => normalizeStepName(i.stepName) === tab).length }}</span>
<el-table-column label="生产日期" prop="prodDate" width="110" align="center" show-overflow-tooltip /> </span>
<el-table-column label="工序类型" prop="actionId" width="100" align="center" show-overflow-tooltip> </div>
<template slot-scope="scope"> <!-- 表格 -->
{{ getActionIdName(scope.row.actionId) }} <el-table
</template> ref="scheduledTable"
</el-table-column> :data="currentStepItems"
<el-table-column label="排产状态" prop="scheduleStatus" width="90" align="center"> border
<template slot-scope="scope"> size="small"
<span class="aps-status-tag" :class="'status-' + (scope.row.scheduleStatus || 1)">{{ statusMap[scope.row.scheduleStatus] || '未知' }}</span> class="aps-table"
</template> >
</el-table-column> <el-table-column label="排产单号" prop="scheduleNo" min-width="140" show-overflow-tooltip />
<el-table-column label="规格" prop="spec" min-width="120" show-overflow-tooltip /> <el-table-column label="生产日期" prop="prodDate" width="110" align="center" show-overflow-tooltip />
<el-table-column label="材质" prop="material" width="90" align="center" show-overflow-tooltip /> <el-table-column label="工序步骤" prop="stepName" width="120" align="center" show-overflow-tooltip />
<el-table-column label="排产吨数" prop="scheduleWeight" width="100" align="right" show-overflow-tooltip /> <el-table-column label="排产状态" prop="scheduleStatus" width="90" align="center">
<el-table-column label="品名项" prop="productItem" min-width="100" show-overflow-tooltip /> <template slot-scope="scope">
<el-table-column label="品名" prop="productType" min-width="90" align="center" show-overflow-tooltip /> <span class="aps-status-tag" :class="'status-' + (scope.row.scheduleStatus || 1)">{{ statusMap[scope.row.scheduleStatus] || '未知' }}</span>
<el-table-column label="订货单位" prop="customerName" min-width="140" show-overflow-tooltip /> </template>
<el-table-column label="业务员" prop="businessUser" width="90" align="center" show-overflow-tooltip /> </el-table-column>
<el-table-column label="联系电话" prop="businessPhone" width="110" align="center" show-overflow-tooltip /> <el-table-column label="规格" prop="spec" min-width="120" show-overflow-tooltip />
<el-table-column label="交货期(天)" prop="deliveryCycle" width="100" align="center" show-overflow-tooltip /> <el-table-column label="材质" prop="material" width="90" align="center" show-overflow-tooltip />
<el-table-column label="产品用途" prop="usePurpose" min-width="120" show-overflow-tooltip /> <el-table-column label="排产吨数" prop="scheduleWeight" width="100" align="right" show-overflow-tooltip />
<el-table-column label="厚度公差" prop="thicknessTolerance" min-width="100" show-overflow-tooltip /> <el-table-column label="品名项" prop="productItem" min-width="100" show-overflow-tooltip />
<el-table-column label="宽度公差" prop="widthTolerance" 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="surfaceQuality" min-width="100" show-overflow-tooltip /> <el-table-column label="订货单位" prop="customerName" min-width="140" show-overflow-tooltip />
<el-table-column label="表面处理" prop="surfaceTreatment" min-width="100" show-overflow-tooltip /> <el-table-column label="业务员" prop="businessUser" width="90" align="center" show-overflow-tooltip />
<el-table-column label="内径尺寸" prop="innerDiameter" 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="outerDiameter" width="90" align="center" show-overflow-tooltip /> <el-table-column label="交货期(天)" prop="deliveryCycle" width="100" align="center" show-overflow-tooltip />
<el-table-column label="包装要求" prop="packReq" min-width="100" show-overflow-tooltip /> <el-table-column label="产品用途" prop="usePurpose" min-width="120" show-overflow-tooltip />
<el-table-column label="切边要求" prop="cutEdgeReq" min-width="100" show-overflow-tooltip /> <el-table-column label="厚度公差" prop="thicknessTolerance" min-width="100" show-overflow-tooltip />
<el-table-column label="单件重量" prop="singleCoilWeight" width="90" align="center" show-overflow-tooltip /> <el-table-column label="宽度公差" prop="widthTolerance" min-width="100" show-overflow-tooltip />
<el-table-column label="交货重量偏差" prop="weightDeviation" min-width="110" show-overflow-tooltip /> <el-table-column label="表面质量" prop="surfaceQuality" min-width="100" show-overflow-tooltip />
<el-table-column label="其他技术要求" prop="otherTechReq" min-width="130" show-overflow-tooltip /> <el-table-column label="表面处理" prop="surfaceTreatment" min-width="100" show-overflow-tooltip />
<el-table-column label="付款情况说明" prop="paymentDesc" min-width="130" show-overflow-tooltip /> <el-table-column label="内径尺寸" prop="innerDiameter" width="90" align="center" show-overflow-tooltip />
<el-table-column label="单行排产备注" prop="rowRemark" min-width="100" show-overflow-tooltip /> <el-table-column label="外径要求" prop="outerDiameter" width="90" align="center" show-overflow-tooltip />
<el-table-column label="备注" prop="remark" min-width="100" show-overflow-tooltip /> <el-table-column label="包装要求" prop="packReq" min-width="100" show-overflow-tooltip />
<el-table-column label="操作" width="150" align="center" fixed="right"> <el-table-column label="切边要求" prop="cutEdgeReq" min-width="100" show-overflow-tooltip />
<template slot-scope="scope"> <el-table-column label="单件重量" prop="singleCoilWeight" width="90" align="center" show-overflow-tooltip />
<div style="display:flex; justify-content:center; gap:0;"> <el-table-column label="交货重量偏差" prop="weightDeviation" min-width="110" show-overflow-tooltip />
<el-button type="text" size="small" style="color:#409EFF;padding:0 6px;" @click="handleEditScheduled(scope.row)">编辑</el-button> <el-table-column label="其他技术要求" prop="otherTechReq" min-width="130" show-overflow-tooltip />
<el-button type="text" size="small" style="color:#ff4d4f;padding:0 6px;" @click="handleDeleteScheduled(scope.row)">删除</el-button> <el-table-column label="付款情况说明" prop="paymentDesc" min-width="130" show-overflow-tooltip />
</div> <el-table-column label="单行排产备注" prop="rowRemark" min-width="100" show-overflow-tooltip />
</template> <el-table-column label="备注" prop="remark" min-width="100" show-overflow-tooltip />
</el-table-column> <el-table-column label="操作" width="150" align="center" fixed="right">
</el-table> <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>
</el-table-column>
</el-table>
</template>
<div v-else-if="!schLoading" style="padding:40px;text-align:center;color:#909399;"> <div v-else-if="!schLoading" style="padding:40px;text-align:center;color:#909399;">
{{ hasQueried ? '该日期暂无已排产数据' : '请选择日期查询' }} {{ hasQueried ? '该日期暂无已排产数据' : '请选择日期查询' }}
</div> </div>
@@ -493,149 +580,48 @@
</div> </div>
</el-dialog> </el-dialog>
<!-- 合并排产明细对话框 --> <!-- 接收产需单弹窗 - 配置工序 -->
<el-dialog <el-dialog
title="合并排产明细" title="接收产需单 - 配置工序"
:visible.sync="mergeDialogVisible" :visible.sync="receiveDialogVisible"
width="850px" width="900px"
append-to-body append-to-body
:close-on-click-modal="false" :close-on-click-modal="false"
> >
<div style="margin-bottom:12px;font-size:13px;color:#e67e22;"> <div style="margin-bottom:12px;font-size:13px;color:#e67e22;">
选中 {{ mergeForm.itemCount }} 条明细请选择一个作为合并模板下方字段将自动填充您可修改后确认合并 请为每条明细配置工序所有明细配置完成后才能接收
</div> </div>
<!-- 选择模板 --> <el-table :data="receiveDetailList" border size="small" class="aps-table" max-height="400">
<el-table :data="mergeSourceRows" border size="small" style="margin-bottom:12px;" @row-click="pickMergeTemplate"> <el-table-column label="规格" prop="spec" min-width="120" show-overflow-tooltip />
<el-table-column width="50" align="center"> <el-table-column label="材质" prop="material" width="90" align="center" show-overflow-tooltip />
<el-table-column label="排产吨数" prop="scheduleWeight" width="100" align="right" />
<el-table-column label="品名" prop="productType" min-width="90" align="center" show-overflow-tooltip />
<el-table-column label="配置工序" min-width="200">
<template slot-scope="scope"> <template slot-scope="scope">
<el-radio v-model="mergeTemplateIndex" :label="scope.$index" @click.native.stop /> <ProcessSelect
v-model="scope.row.processId"
placeholder="请选择工序"
@change="(val, data) => handleReceiveProcessChange(scope.row, val, data)"
/>
</template>
</el-table-column>
<el-table-column label="工序步骤" min-width="300">
<template slot-scope="scope">
<div v-if="scope.row.stepList && scope.row.stepList.length > 0" class="receive-steps-flow">
<template v-for="(step, index) in scope.row.stepList">
<span :key="step.stepId" class="receive-step-tag">{{ step.stepName }}</span>
<span v-if="index < scope.row.stepList.length - 1" :key="'arrow-' + index" class="receive-step-arrow"></span>
</template>
</div>
<span v-else style="color:#909399;font-size:12px;"></span>
</template> </template>
</el-table-column> </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-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-select v-model="mergeForm.actionId" placeholder="请选择工序类型" clearable filterable style="width:100%">
<el-option v-for="p in processOptions" :key="p.actionId" :label="p.name" :value="p.actionId" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="订货单位"><el-input v-model="mergeForm.customerName" /></el-form-item>
</el-col>
<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"> <div slot="footer" class="dialog-footer">
<el-button :loading="mergeBtnLoading" type="warning" @click="confirmMerge"> </el-button> <el-button :loading="receiveBtnLoading" type="primary" :disabled="!canConfirmReceive" @click="confirmReceive"> </el-button>
<el-button @click="mergeDialogVisible = false"> </el-button> <el-button @click="receiveDialogVisible = false"> </el-button>
</div> </div>
</el-dialog> </el-dialog>
</div> </div>
@@ -651,13 +637,14 @@ import {
listScheduleItem, listScheduleItem,
updateScheduleItem, updateScheduleItem,
delScheduleItem, delScheduleItem,
receiveScheduleItem, receiveScheduleItem
mergeScheduleItem
} from '@/api/aps/schedule' } from '@/api/aps/schedule'
import { PROCESSES } from '@/utils/meta' import { PROCESSES } from '@/utils/meta'
import ProcessSelect from '@/components/KLPService/ProcessSelect/index.vue'
export default { export default {
name: 'ApsSchedule', name: 'ApsSchedule',
components: { ProcessSelect },
data() { data() {
const today = new Date() const today = new Date()
const y = today.getFullYear() const y = today.getFullYear()
@@ -676,26 +663,13 @@ export default {
statusMap: { 1: '待审核', 2: '已接收', 3: '已驳回' }, statusMap: { 1: '待审核', 2: '已接收', 3: '已驳回' },
processOptions: PROCESSES, processOptions: PROCESSES,
// 已接收
acceptedScheduleList: [],
acceptedLoading: false,
// 已排产 // 已排产
scheduledItemList: [], scheduledItemList: [],
selectedItems: [], scheduledStepTab: '',
sourceColorMap: {},
// 合并对话框
mergeDialogVisible: false,
mergeBtnLoading: false,
mergeTemplateIndex: 0,
mergeSourceRows: [],
mergeForm: {
itemCount: 0, scheduleNo: '', actionId: '', 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,
@@ -714,14 +688,37 @@ export default {
// 下钻 // 下钻
drillDialogVisible: false, drillDialogVisible: false,
drillOrder: null drillOrder: null,
// 接收产需单弹窗
receiveDialogVisible: false,
receiveBtnLoading: false,
receiveScheduleId: null,
receiveDetailList: []
} }
}, },
watch: { computed: {
activeTab() { canConfirmReceive() {
this.selectedItems = [] if (!this.receiveDetailList || this.receiveDetailList.length === 0) return false
return this.receiveDetailList.every(d => d.processId)
},
// 已排产按步骤分组:去掉括号及括号内文字
scheduledStepTabs() {
const names = this.scheduledItemList
.map(item => this.normalizeStepName(item.stepName))
.filter(Boolean)
return [...new Set(names)]
},
// 当前选中的步骤tab对应的数据
currentStepItems() {
if (!this.scheduledStepTab) return this.scheduledItemList
const tab = this.scheduledStepTab
return this.scheduledItemList.filter(
item => this.normalizeStepName(item.stepName) === tab
)
} }
}, },
watch: {},
created() { created() {
this.handleQuery() this.handleQuery()
}, },
@@ -775,6 +772,8 @@ export default {
this.hasQueried = true this.hasQueried = true
if (this.activeTab === 'pending') { if (this.activeTab === 'pending') {
this.queryPending() this.queryPending()
} else if (this.activeTab === 'accepted') {
this.queryAccepted()
} else { } else {
this.queryScheduled() this.queryScheduled()
} }
@@ -808,25 +807,82 @@ export default {
}) })
}, },
// ====== 已接收 ======
queryAccepted() {
this.acceptedLoading = true
this.acceptedScheduleList = []
listRequirement({
prodDate: this.queryDate,
scheduleStatus: 2,
pageNum: 1,
pageSize: 999
}).then(res => {
this.acceptedScheduleList = res.rows || []
const totalCount = this.acceptedScheduleList.reduce((sum, sch) => sum + (sch.detailList || []).length, 0)
this.summaryText = `${this.acceptedScheduleList.length} 个已接收产需单,${totalCount} 条明细`
}).catch(() => {
this.acceptedScheduleList = []
this.summaryText = ''
}).finally(() => {
this.acceptedLoading = false
})
},
handleAccept(sch) { handleAccept(sch) {
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( // 初始化接收明细列表,为每条明细添加 processId 和 stepList 字段
`确认接收产需单「${sch.scheduleNo}」的全部排产明细吗?共 ${details.length} 条明细`, this.receiveScheduleId = sch.scheduleId
'提示', this.receiveDetailList = details.map(d => ({
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' } ...d,
).then(() => { processId: undefined,
this.$message.info('正在处理接收...') stepList: []
receiveScheduleItem(sch.scheduleId).then(() => { }))
this.$modal.msgSuccess('接收成功,排产明细已写入') this.receiveDialogVisible = true
this.queryPending() },
}).catch(() => {
this.$modal.msgError('接收失败') handleReceiveProcessChange(row, processId, processData) {
}) if (processData && processData.stepList) {
}).catch(() => {}) row.stepList = processData.stepList
} else {
row.stepList = []
}
},
confirmReceive() {
// 验证所有明细都配置了工序
const unconfigured = this.receiveDetailList.filter(d => !d.processId)
if (unconfigured.length > 0) {
this.$message.warning(`还有 ${unconfigured.length} 条明细未配置工序,请为所有明细配置工序`)
return
}
// 构建接收数据,传递每条明细的 processId 和 stepList
const receiveData = {
scheduleId: this.receiveScheduleId,
detailProcessList: this.receiveDetailList.map(d => ({
scheduleDetailId: d.scheduleDetailId || d.scheduleDetailIds,
processId: d.processId,
stepList: (d.stepList || []).map(s => ({
stepId: s.stepId,
stepOrder: s.stepOrder,
stepName: s.stepName
}))
}))
}
this.receiveBtnLoading = true
receiveScheduleItem(receiveData).then(() => {
this.$modal.msgSuccess('接收成功,排产明细已写入')
this.receiveDialogVisible = false
this.queryPending()
}).catch(() => {
this.$modal.msgError('接收失败')
}).finally(() => {
this.receiveBtnLoading = false
})
}, },
handleReject(sch) { handleReject(sch) {
@@ -864,16 +920,14 @@ export default {
queryScheduled() { queryScheduled() {
this.schLoading = true this.schLoading = true
this.scheduledItemList = [] this.scheduledItemList = []
this.sourceColorMap = {} this.scheduledStepTab = ''
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 || []).sort((a, b) => (a.scheduleNo || '').localeCompare(b.scheduleNo || '')) this.scheduledItemList = (res.rows || []).sort((a, b) => (a.scheduleNo || '').localeCompare(b.scheduleNo || ''))
try { // 默认选中第一个步骤tab
this.sourceColorMap = this.buildGroupColorMap(this.scheduledItemList) const tabs = this.scheduledStepTabs
} catch (e) { if (tabs.length > 0) {
console.error('buildGroupColorMap error:', e) this.scheduledStepTab = tabs[0]
this.sourceColorMap = {}
} }
const totalWeight = this.scheduledItemList.reduce((sum, d) => { const totalWeight = this.scheduledItemList.reduce((sum, d) => {
const w = parseFloat(d.scheduleWeight) const w = parseFloat(d.scheduleWeight)
@@ -934,98 +988,6 @@ 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 || '',
actionId: row.actionId != null ? Number(row.actionId) : '',
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 || []
@@ -1038,6 +1000,12 @@ export default {
return p ? p.name : (actionId || '') return p ? p.name : (actionId || '')
}, },
// 去掉步骤名称中的括号及括号内文字:镀锌(毛化)=> 镀锌
normalizeStepName(name) {
if (!name) return ''
return name.replace(/[(][^)]*[)]/g, '').trim()
},
handleDetailClick(sch, detail) { handleDetailClick(sch, detail) {
// 点击明细行查看来源订单 // 点击明细行查看来源订单
if (!sch || !sch.orderList || sch.orderList.length === 0) { if (!sch || !sch.orderList || sch.orderList.length === 0) {
@@ -1350,20 +1318,85 @@ export default {
font-size: 12px; font-size: 12px;
} }
// ====== 合并候选行颜色高亮(同一源排产单 → 同色) ====== // ====== 接收弹窗步骤展示 ======
::v-deep .el-table__body tr.merge-source-0 > td { .receive-steps-flow {
background-color: #f0f5ff !important; display: flex;
flex-wrap: wrap;
align-items: center;
gap: 2px;
} }
::v-deep .el-table__body tr.merge-source-1 > td {
background-color: #f6ffed !important; .receive-step-tag {
display: inline-block;
padding: 2px 8px;
background: $aps-silver-1;
border: 1px solid $aps-silver-mid;
border-radius: 3px;
font-size: 12px;
color: $aps-text;
} }
::v-deep .el-table__body tr.merge-source-2 > td {
background-color: #fff7e6 !important; .receive-step-arrow {
color: $aps-red-2;
font-size: 14px;
padding: 0 2px;
} }
::v-deep .el-table__body tr.merge-source-3 > td {
background-color: #f9f0ff !important; // ====== 已排产步骤工序 Tab ======
.aps-step-tabs {
display: flex;
flex-wrap: wrap;
padding: 8px 12px;
gap: 6px;
background: $aps-silver-1;
border-bottom: 1px solid $aps-border;
} }
::v-deep .el-table__body tr.merge-source-4 > td {
background-color: #fff0f6 !important; .aps-step-tab {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 14px;
font-size: 12px;
font-weight: 500;
color: $aps-text-muted;
background: $aps-white;
border: 1px solid $aps-silver-mid;
border-radius: $aps-radius;
cursor: pointer;
transition: all 0.2s;
user-select: none;
&:hover {
color: $aps-red-2;
border-color: $aps-red-2;
}
}
.aps-step-tab-active {
color: #fff;
background: $aps-red-2;
border-color: $aps-red-2;
&:hover {
color: #fff;
}
}
.aps-step-tab-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 16px;
padding: 0 5px;
font-size: 10px;
font-weight: 600;
border-radius: 8px;
background: rgba(0, 0, 0, 0.1);
.aps-step-tab-active & {
background: rgba(255, 255, 255, 0.25);
}
} }
</style> </style>