This commit is contained in:
jhd
2026-07-03 17:56:05 +08:00
65 changed files with 2704 additions and 730 deletions

View File

@@ -0,0 +1,99 @@
package com.klp.flow.controller;
import java.util.List;
import java.util.Arrays;
import lombok.RequiredArgsConstructor;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import com.klp.common.annotation.RepeatSubmit;
import com.klp.common.annotation.Log;
import com.klp.common.core.controller.BaseController;
import com.klp.common.core.domain.PageQuery;
import com.klp.common.core.domain.R;
import com.klp.common.core.validate.AddGroup;
import com.klp.common.core.validate.EditGroup;
import com.klp.common.enums.BusinessType;
import com.klp.common.utils.poi.ExcelUtil;
import com.klp.flow.domain.vo.SchProdProcessVo;
import com.klp.flow.domain.bo.SchProdProcessBo;
import com.klp.flow.service.ISchProdProcessService;
import com.klp.common.core.page.TableDataInfo;
/**
* 工序定义主
*
* @author klp
* @date 2026-07-03
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/flow/prodProcess")
public class SchProdProcessController extends BaseController {
private final ISchProdProcessService iSchProdProcessService;
/**
* 查询工序定义主列表
*/
@GetMapping("/list")
public TableDataInfo<SchProdProcessVo> list(SchProdProcessBo bo, PageQuery pageQuery) {
return iSchProdProcessService.queryPageList(bo, pageQuery);
}
/**
* 导出工序定义主列表
*/
@Log(title = "工序定义主", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(SchProdProcessBo bo, HttpServletResponse response) {
List<SchProdProcessVo> list = iSchProdProcessService.queryList(bo);
ExcelUtil.exportExcel(list, "工序定义主", SchProdProcessVo.class, response);
}
/**
* 获取工序定义主详细信息
*
* @param processId 主键
*/
@GetMapping("/{processId}")
public R<SchProdProcessVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long processId) {
return R.ok(iSchProdProcessService.queryById(processId));
}
/**
* 新增工序定义主
*/
@Log(title = "工序定义主", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody SchProdProcessBo bo) {
return toAjax(iSchProdProcessService.insertByBo(bo));
}
/**
* 修改工序定义主
*/
@Log(title = "工序定义主", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody SchProdProcessBo bo) {
return toAjax(iSchProdProcessService.updateByBo(bo));
}
/**
* 删除工序定义主
*
* @param processIds 主键串
*/
@Log(title = "工序定义主", businessType = BusinessType.DELETE)
@DeleteMapping("/{processIds}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] processIds) {
return toAjax(iSchProdProcessService.deleteWithValidByIds(Arrays.asList(processIds), true));
}
}

View File

@@ -0,0 +1,99 @@
package com.klp.flow.controller;
import java.util.List;
import java.util.Arrays;
import lombok.RequiredArgsConstructor;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import com.klp.common.annotation.RepeatSubmit;
import com.klp.common.annotation.Log;
import com.klp.common.core.controller.BaseController;
import com.klp.common.core.domain.PageQuery;
import com.klp.common.core.domain.R;
import com.klp.common.core.validate.AddGroup;
import com.klp.common.core.validate.EditGroup;
import com.klp.common.enums.BusinessType;
import com.klp.common.utils.poi.ExcelUtil;
import com.klp.flow.domain.vo.SchProdProcessStepVo;
import com.klp.flow.domain.bo.SchProdProcessStepBo;
import com.klp.flow.service.ISchProdProcessStepService;
import com.klp.common.core.page.TableDataInfo;
/**
* 工序步骤明细
*
* @author klp
* @date 2026-07-03
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/flow/prodProcessStep")
public class SchProdProcessStepController extends BaseController {
private final ISchProdProcessStepService iSchProdProcessStepService;
/**
* 查询工序步骤明细列表
*/
@GetMapping("/list")
public TableDataInfo<SchProdProcessStepVo> list(SchProdProcessStepBo bo, PageQuery pageQuery) {
return iSchProdProcessStepService.queryPageList(bo, pageQuery);
}
/**
* 导出工序步骤明细列表
*/
@Log(title = "工序步骤明细", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(SchProdProcessStepBo bo, HttpServletResponse response) {
List<SchProdProcessStepVo> list = iSchProdProcessStepService.queryList(bo);
ExcelUtil.exportExcel(list, "工序步骤明细", SchProdProcessStepVo.class, response);
}
/**
* 获取工序步骤明细详细信息
*
* @param stepId 主键
*/
@GetMapping("/{stepId}")
public R<SchProdProcessStepVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long stepId) {
return R.ok(iSchProdProcessStepService.queryById(stepId));
}
/**
* 新增工序步骤明细
*/
@Log(title = "工序步骤明细", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody SchProdProcessStepBo bo) {
return toAjax(iSchProdProcessStepService.insertByBo(bo));
}
/**
* 修改工序步骤明细
*/
@Log(title = "工序步骤明细", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody SchProdProcessStepBo bo) {
return toAjax(iSchProdProcessStepService.updateByBo(bo));
}
/**
* 删除工序步骤明细
*
* @param stepIds 主键串
*/
@Log(title = "工序步骤明细", businessType = BusinessType.DELETE)
@DeleteMapping("/{stepIds}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] stepIds) {
return toAjax(iSchProdProcessStepService.deleteWithValidByIds(Arrays.asList(stepIds), true));
}
}

View File

@@ -51,6 +51,10 @@ public class EqpMaintenancePlan extends BaseEntity {
* 审批状态0=草稿 1=待审批 2=已审批 3=已驳回
*/
private Long approvalStatus;
/**
* 产线
*/
private String productionLine;
/**
* 计划开始时间
*/

View File

@@ -31,14 +31,6 @@ public class EqpMaintenancePlanDetail extends BaseEntity {
* 维修计划ID
*/
private Long planId;
/**
* 巡检记录ID来源异常工单
*/
private Long recordId;
/**
* 设备ID用户手动选择关联 eqp_equipment_management.equipment_id
*/
private Long equipmentId;
/**
* 产线名称
*/

View File

@@ -0,0 +1,45 @@
package com.klp.flow.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.klp.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 工序定义主对象 sch_prod_process
*
* @author klp
* @date 2026-07-03
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sch_prod_process")
public class SchProdProcess extends BaseEntity {
private static final long serialVersionUID=1L;
/**
* 工序主键ID
*/
@TableId(value = "process_id")
private Long processId;
/**
* 工序名称(如:酸扎镀锌毛化流程)
*/
private String processName;
/**
* 工序描述
*/
private String processDesc;
/**
* 备注
*/
private String remark;
/**
* 删除标识 0正常 2删除
*/
@TableLogic
private Long delFlag;
}

View File

@@ -0,0 +1,49 @@
package com.klp.flow.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.klp.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 工序步骤明细对象 sch_prod_process_step
*
* @author klp
* @date 2026-07-03
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sch_prod_process_step")
public class SchProdProcessStep extends BaseEntity {
private static final long serialVersionUID=1L;
/**
* 步骤主键ID
*/
@TableId(value = "step_id")
private Long stepId;
/**
* 关联工序主表IDsch_prod_process.process_id
*/
private Long processId;
/**
* 步骤顺序号1,2,3...
*/
private Long stepOrder;
/**
* 步骤名称(如:酸扎、镀锌、毛化辊、毛化镀锌卷)
*/
private String stepName;
/**
* 备注
*/
private String remark;
/**
* 删除标识 0正常 2删除
*/
@TableLogic
private Long delFlag;
}

View File

@@ -33,6 +33,10 @@ public class SchProdScheduleDetail extends BaseEntity {
* 来源销售订单明细ID溯源原始订单规格
*/
private Long orderDetailId;
/**
* 关联工序主表IDsch_prod_process.process_id表示该明细使用的工序配置
*/
private Long processId;
/**
* 规格 例1.0X1250
*/

View File

@@ -132,9 +132,9 @@ public class SchProdScheduleItem extends BaseEntity {
*/
private String scheduleDetailIds;
/**
* 工序类型
* 工序ID
*/
private String actionType;
private Long actionId;
/**
* 规格 例1.0X1250
*/

View File

@@ -36,6 +36,11 @@ public class EqpMaintenancePlanBo extends BaseEntity {
*/
private String planName;
/**
* 产线
*/
private String productionLine;
/**
* 维修类型1=定期保养 2=安全整改 3=专项检修 4=故障维修
*/

View File

@@ -8,6 +8,7 @@ import javax.validation.constraints.*;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
/**
* 维修计划明细业务对象 eqp_maintenance_plan_detail
@@ -30,16 +31,6 @@ public class EqpMaintenancePlanDetailBo extends BaseEntity {
*/
private Long planId;
/**
* 巡检记录ID来源异常工单
*/
private Long recordId;
/**
* 设备ID用户手动选择关联 eqp_equipment_management.equipment_id
*/
private Long equipmentId;
/**
* 产线名称
*/
@@ -53,6 +44,8 @@ public class EqpMaintenancePlanDetailBo extends BaseEntity {
/**
* 单条维修项计划执行日期
*/
@DateTimeFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd")
private Date itemPlanDate;
/**

View File

@@ -0,0 +1,41 @@
package com.klp.flow.domain.bo;
import com.klp.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.*;
/**
* 工序定义主业务对象 sch_prod_process
*
* @author klp
* @date 2026-07-03
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class SchProdProcessBo extends BaseEntity {
/**
* 工序主键ID
*/
private Long processId;
/**
* 工序名称(如:酸扎镀锌毛化流程)
*/
private String processName;
/**
* 工序描述
*/
private String processDesc;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,46 @@
package com.klp.flow.domain.bo;
import com.klp.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.*;
/**
* 工序步骤明细业务对象 sch_prod_process_step
*
* @author klp
* @date 2026-07-03
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class SchProdProcessStepBo extends BaseEntity {
/**
* 步骤主键ID
*/
private Long stepId;
/**
* 关联工序主表IDsch_prod_process.process_id
*/
private Long processId;
/**
* 步骤顺序号1,2,3...
*/
private Long stepOrder;
/**
* 步骤名称(如:酸扎、镀锌、毛化辊、毛化镀锌卷)
*/
private String stepName;
/**
* 备注
*/
private String remark;
}

View File

@@ -33,6 +33,11 @@ public class SchProdScheduleDetailBo extends BaseEntity {
*/
private Long orderDetailId;
/**
* 关联工序主表IDsch_prod_process.process_id表示该明细使用的工序配置
*/
private Long processId;
/**
* 规格 例1.0X1250
*/

View File

@@ -159,9 +159,9 @@ public class SchProdScheduleItemBo extends BaseEntity {
private String scheduleDetailIds;
/**
* 工序类型
* 工序ID
*/
private String actionType;
private Long actionId;
/**
* 规格 例1.0X1250

View File

@@ -34,20 +34,6 @@ public class EqpMaintenancePlanDetailVo {
@ExcelProperty(value = "维修计划ID")
private Long planId;
/**
* 巡检记录ID来源异常工单
*/
@ExcelProperty(value = "巡检记录ID", converter = ExcelDictConvert.class)
@ExcelDictFormat(readConverterExp = "来=源异常工单")
private Long recordId;
/**
* 设备ID用户手动选择关联 eqp_equipment_management.equipment_id
*/
@ExcelProperty(value = "设备ID", converter = ExcelDictConvert.class)
@ExcelDictFormat(readConverterExp = "用=户手动选择,关联,e=qp_equipment_management.equipment_id")
private Long equipmentId;
/**
* 产线名称
*/

View File

@@ -40,6 +40,12 @@ public class EqpMaintenancePlanVo {
@ExcelProperty(value = "计划名称")
private String planName;
/**
* 产线
*/
@ExcelProperty(value = "产线")
private String productionLine;
/**
* 维修类型1=定期保养 2=安全整改 3=专项检修 4=故障维修
*/

View File

@@ -0,0 +1,56 @@
package com.klp.flow.domain.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import com.klp.common.annotation.ExcelDictFormat;
import com.klp.common.convert.ExcelDictConvert;
import lombok.Data;
/**
* 工序步骤明细视图对象 sch_prod_process_step
*
* @author klp
* @date 2026-07-03
*/
@Data
@ExcelIgnoreUnannotated
public class SchProdProcessStepVo {
private static final long serialVersionUID = 1L;
/**
* 步骤主键ID
*/
@ExcelProperty(value = "步骤主键ID")
private Long stepId;
/**
* 关联工序主表IDsch_prod_process.process_id
*/
@ExcelProperty(value = "关联工序主表ID", converter = ExcelDictConvert.class)
@ExcelDictFormat(readConverterExp = "s=ch_prod_process.process_id")
private Long processId;
/**
* 步骤顺序号1,2,3...
*/
@ExcelProperty(value = "步骤顺序号", converter = ExcelDictConvert.class)
@ExcelDictFormat(readConverterExp = "1=,2,3...")
private Long stepOrder;
/**
* 步骤名称(如:酸扎、镀锌、毛化辊、毛化镀锌卷)
*/
@ExcelProperty(value = "步骤名称", converter = ExcelDictConvert.class)
@ExcelDictFormat(readConverterExp = "如=:酸扎、镀锌、毛化辊、毛化镀锌卷")
private String stepName;
/**
* 备注
*/
@ExcelProperty(value = "备注")
private String remark;
}

View File

@@ -0,0 +1,48 @@
package com.klp.flow.domain.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import com.klp.common.annotation.ExcelDictFormat;
import com.klp.common.convert.ExcelDictConvert;
import lombok.Data;
/**
* 工序定义主视图对象 sch_prod_process
*
* @author klp
* @date 2026-07-03
*/
@Data
@ExcelIgnoreUnannotated
public class SchProdProcessVo {
private static final long serialVersionUID = 1L;
/**
* 工序主键ID
*/
@ExcelProperty(value = "工序主键ID")
private Long processId;
/**
* 工序名称(如:酸扎镀锌毛化流程)
*/
@ExcelProperty(value = "工序名称", converter = ExcelDictConvert.class)
@ExcelDictFormat(readConverterExp = "如=:酸扎镀锌毛化流程")
private String processName;
/**
* 工序描述
*/
@ExcelProperty(value = "工序描述")
private String processDesc;
/**
* 备注
*/
@ExcelProperty(value = "备注")
private String remark;
}

View File

@@ -40,6 +40,12 @@ public class SchProdScheduleDetailVo {
@ExcelDictFormat(readConverterExp = "溯=源原始订单规格")
private Long orderDetailId;
/**
* 关联工序主表IDsch_prod_process.process_id表示该明细使用的工序配置
*/
@ExcelProperty(value = "关联工序主表ID")
private Long processId;
/**
* 规格 例1.0X1250
*/

View File

@@ -190,10 +190,10 @@ public class SchProdScheduleItemVo {
private String scheduleDetailIds;
/**
* 工序类型
* 工序ID
*/
@ExcelProperty(value = "工序类型")
private String actionType;
@ExcelProperty(value = "工序ID")
private Long actionId;
/**
* 规格 例1.0X1250

View File

@@ -0,0 +1,15 @@
package com.klp.flow.mapper;
import com.klp.flow.domain.SchProdProcess;
import com.klp.flow.domain.vo.SchProdProcessVo;
import com.klp.common.core.mapper.BaseMapperPlus;
/**
* 工序定义主Mapper接口
*
* @author klp
* @date 2026-07-03
*/
public interface SchProdProcessMapper extends BaseMapperPlus<SchProdProcessMapper, SchProdProcess, SchProdProcessVo> {
}

View File

@@ -0,0 +1,15 @@
package com.klp.flow.mapper;
import com.klp.flow.domain.SchProdProcessStep;
import com.klp.flow.domain.vo.SchProdProcessStepVo;
import com.klp.common.core.mapper.BaseMapperPlus;
/**
* 工序步骤明细Mapper接口
*
* @author klp
* @date 2026-07-03
*/
public interface SchProdProcessStepMapper extends BaseMapperPlus<SchProdProcessStepMapper, SchProdProcessStep, SchProdProcessStepVo> {
}

View File

@@ -0,0 +1,49 @@
package com.klp.flow.service;
import com.klp.flow.domain.SchProdProcess;
import com.klp.flow.domain.vo.SchProdProcessVo;
import com.klp.flow.domain.bo.SchProdProcessBo;
import com.klp.common.core.page.TableDataInfo;
import com.klp.common.core.domain.PageQuery;
import java.util.Collection;
import java.util.List;
/**
* 工序定义主Service接口
*
* @author klp
* @date 2026-07-03
*/
public interface ISchProdProcessService {
/**
* 查询工序定义主
*/
SchProdProcessVo queryById(Long processId);
/**
* 查询工序定义主列表
*/
TableDataInfo<SchProdProcessVo> queryPageList(SchProdProcessBo bo, PageQuery pageQuery);
/**
* 查询工序定义主列表
*/
List<SchProdProcessVo> queryList(SchProdProcessBo bo);
/**
* 新增工序定义主
*/
Boolean insertByBo(SchProdProcessBo bo);
/**
* 修改工序定义主
*/
Boolean updateByBo(SchProdProcessBo bo);
/**
* 校验并批量删除工序定义主信息
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
}

View File

@@ -0,0 +1,49 @@
package com.klp.flow.service;
import com.klp.flow.domain.SchProdProcessStep;
import com.klp.flow.domain.vo.SchProdProcessStepVo;
import com.klp.flow.domain.bo.SchProdProcessStepBo;
import com.klp.common.core.page.TableDataInfo;
import com.klp.common.core.domain.PageQuery;
import java.util.Collection;
import java.util.List;
/**
* 工序步骤明细Service接口
*
* @author klp
* @date 2026-07-03
*/
public interface ISchProdProcessStepService {
/**
* 查询工序步骤明细
*/
SchProdProcessStepVo queryById(Long stepId);
/**
* 查询工序步骤明细列表
*/
TableDataInfo<SchProdProcessStepVo> queryPageList(SchProdProcessStepBo bo, PageQuery pageQuery);
/**
* 查询工序步骤明细列表
*/
List<SchProdProcessStepVo> queryList(SchProdProcessStepBo bo);
/**
* 新增工序步骤明细
*/
Boolean insertByBo(SchProdProcessStepBo bo);
/**
* 修改工序步骤明细
*/
Boolean updateByBo(SchProdProcessStepBo bo);
/**
* 校验并批量删除工序步骤明细信息
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
}

View File

@@ -62,8 +62,6 @@ public class EqpMaintenancePlanDetailServiceImpl implements IEqpMaintenancePlanD
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<EqpMaintenancePlanDetail> lqw = Wrappers.lambdaQuery();
lqw.eq(bo.getPlanId() != null, EqpMaintenancePlanDetail::getPlanId, bo.getPlanId());
lqw.eq(bo.getRecordId() != null, EqpMaintenancePlanDetail::getRecordId, bo.getRecordId());
lqw.eq(bo.getEquipmentId() != null, EqpMaintenancePlanDetail::getEquipmentId, bo.getEquipmentId());
lqw.eq(StringUtils.isNotBlank(bo.getProductionLine()), EqpMaintenancePlanDetail::getProductionLine, bo.getProductionLine());
lqw.like(StringUtils.isNotBlank(bo.getComponentName()), EqpMaintenancePlanDetail::getComponentName, bo.getComponentName());
lqw.eq(bo.getItemPlanDate() != null, EqpMaintenancePlanDetail::getItemPlanDate, bo.getItemPlanDate());

View File

@@ -63,6 +63,7 @@ public class EqpMaintenancePlanServiceImpl implements IEqpMaintenancePlanService
LambdaQueryWrapper<EqpMaintenancePlan> lqw = Wrappers.lambdaQuery();
lqw.eq(StringUtils.isNotBlank(bo.getPlanNo()), EqpMaintenancePlan::getPlanNo, bo.getPlanNo());
lqw.like(StringUtils.isNotBlank(bo.getPlanName()), EqpMaintenancePlan::getPlanName, bo.getPlanName());
lqw.eq(StringUtils.isNotBlank(bo.getProductionLine()), EqpMaintenancePlan::getProductionLine, bo.getProductionLine());
lqw.eq(bo.getRepairType() != null, EqpMaintenancePlan::getRepairType, bo.getRepairType());
lqw.eq(bo.getPriorityLevel() != null, EqpMaintenancePlan::getPriorityLevel, bo.getPriorityLevel());
lqw.eq(bo.getPlanStatus() != null, EqpMaintenancePlan::getPlanStatus, bo.getPlanStatus());

View File

@@ -0,0 +1,110 @@
package com.klp.flow.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.klp.common.core.page.TableDataInfo;
import com.klp.common.core.domain.PageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.klp.common.utils.StringUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import com.klp.flow.domain.bo.SchProdProcessBo;
import com.klp.flow.domain.vo.SchProdProcessVo;
import com.klp.flow.domain.SchProdProcess;
import com.klp.flow.mapper.SchProdProcessMapper;
import com.klp.flow.service.ISchProdProcessService;
import java.util.List;
import java.util.Map;
import java.util.Collection;
/**
* 工序定义主Service业务层处理
*
* @author klp
* @date 2026-07-03
*/
@RequiredArgsConstructor
@Service
public class SchProdProcessServiceImpl implements ISchProdProcessService {
private final SchProdProcessMapper baseMapper;
/**
* 查询工序定义主
*/
@Override
public SchProdProcessVo queryById(Long processId){
return baseMapper.selectVoById(processId);
}
/**
* 查询工序定义主列表
*/
@Override
public TableDataInfo<SchProdProcessVo> queryPageList(SchProdProcessBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<SchProdProcess> lqw = buildQueryWrapper(bo);
Page<SchProdProcessVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询工序定义主列表
*/
@Override
public List<SchProdProcessVo> queryList(SchProdProcessBo bo) {
LambdaQueryWrapper<SchProdProcess> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<SchProdProcess> buildQueryWrapper(SchProdProcessBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<SchProdProcess> lqw = Wrappers.lambdaQuery();
lqw.like(StringUtils.isNotBlank(bo.getProcessName()), SchProdProcess::getProcessName, bo.getProcessName());
lqw.eq(StringUtils.isNotBlank(bo.getProcessDesc()), SchProdProcess::getProcessDesc, bo.getProcessDesc());
return lqw;
}
/**
* 新增工序定义主
*/
@Override
public Boolean insertByBo(SchProdProcessBo bo) {
SchProdProcess add = BeanUtil.toBean(bo, SchProdProcess.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setProcessId(add.getProcessId());
}
return flag;
}
/**
* 修改工序定义主
*/
@Override
public Boolean updateByBo(SchProdProcessBo bo) {
SchProdProcess update = BeanUtil.toBean(bo, SchProdProcess.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(SchProdProcess entity){
//TODO 做一些数据校验,如唯一约束
}
/**
* 批量删除工序定义主
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteBatchIds(ids) > 0;
}
}

View File

@@ -0,0 +1,111 @@
package com.klp.flow.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.klp.common.core.page.TableDataInfo;
import com.klp.common.core.domain.PageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.klp.common.utils.StringUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import com.klp.flow.domain.bo.SchProdProcessStepBo;
import com.klp.flow.domain.vo.SchProdProcessStepVo;
import com.klp.flow.domain.SchProdProcessStep;
import com.klp.flow.mapper.SchProdProcessStepMapper;
import com.klp.flow.service.ISchProdProcessStepService;
import java.util.List;
import java.util.Map;
import java.util.Collection;
/**
* 工序步骤明细Service业务层处理
*
* @author klp
* @date 2026-07-03
*/
@RequiredArgsConstructor
@Service
public class SchProdProcessStepServiceImpl implements ISchProdProcessStepService {
private final SchProdProcessStepMapper baseMapper;
/**
* 查询工序步骤明细
*/
@Override
public SchProdProcessStepVo queryById(Long stepId){
return baseMapper.selectVoById(stepId);
}
/**
* 查询工序步骤明细列表
*/
@Override
public TableDataInfo<SchProdProcessStepVo> queryPageList(SchProdProcessStepBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<SchProdProcessStep> lqw = buildQueryWrapper(bo);
Page<SchProdProcessStepVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询工序步骤明细列表
*/
@Override
public List<SchProdProcessStepVo> queryList(SchProdProcessStepBo bo) {
LambdaQueryWrapper<SchProdProcessStep> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<SchProdProcessStep> buildQueryWrapper(SchProdProcessStepBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<SchProdProcessStep> lqw = Wrappers.lambdaQuery();
lqw.eq(bo.getProcessId() != null, SchProdProcessStep::getProcessId, bo.getProcessId());
lqw.eq(bo.getStepOrder() != null, SchProdProcessStep::getStepOrder, bo.getStepOrder());
lqw.like(StringUtils.isNotBlank(bo.getStepName()), SchProdProcessStep::getStepName, bo.getStepName());
return lqw;
}
/**
* 新增工序步骤明细
*/
@Override
public Boolean insertByBo(SchProdProcessStepBo bo) {
SchProdProcessStep add = BeanUtil.toBean(bo, SchProdProcessStep.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setStepId(add.getStepId());
}
return flag;
}
/**
* 修改工序步骤明细
*/
@Override
public Boolean updateByBo(SchProdProcessStepBo bo) {
SchProdProcessStep update = BeanUtil.toBean(bo, SchProdProcessStep.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(SchProdProcessStep entity){
//TODO 做一些数据校验,如唯一约束
}
/**
* 批量删除工序步骤明细
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteBatchIds(ids) > 0;
}
}

View File

@@ -99,6 +99,7 @@ public class SchProdScheduleDetailServiceImpl implements ISchProdScheduleDetailS
LambdaQueryWrapper<SchProdScheduleDetail> lqw = Wrappers.lambdaQuery();
lqw.eq(bo.getScheduleId() != null, SchProdScheduleDetail::getScheduleId, bo.getScheduleId());
lqw.eq(bo.getOrderDetailId() != null, SchProdScheduleDetail::getOrderDetailId, bo.getOrderDetailId());
lqw.eq(bo.getProcessId() != null, SchProdScheduleDetail::getProcessId, bo.getProcessId());
lqw.eq(StringUtils.isNotBlank(bo.getSpec()), SchProdScheduleDetail::getSpec, bo.getSpec());
lqw.eq(StringUtils.isNotBlank(bo.getMaterial()), SchProdScheduleDetail::getMaterial, bo.getMaterial());
lqw.eq(bo.getScheduleWeight() != null, SchProdScheduleDetail::getScheduleWeight, bo.getScheduleWeight());

View File

@@ -108,8 +108,8 @@ public class SchProdScheduleItemServiceImpl implements ISchProdScheduleItemServi
lqw.eq(bo.getScheduleWeight() != null, SchProdScheduleItem::getScheduleWeight, bo.getScheduleWeight());
lqw.eq(StringUtils.isNotBlank(bo.getProductItem()), SchProdScheduleItem::getProductItem, bo.getProductItem());
lqw.eq(StringUtils.isNotBlank(bo.getRowRemark()), SchProdScheduleItem::getRowRemark, bo.getRowRemark());
// actionType
lqw.eq(StringUtils.isNotBlank(bo.getActionType()), SchProdScheduleItem::getActionType, bo.getActionType());
// actionId
lqw.eq(bo.getActionId() != null, SchProdScheduleItem::getActionId, bo.getActionId());
return lqw;
}

View File

@@ -7,8 +7,6 @@
<resultMap type="com.klp.flow.domain.EqpMaintenancePlanDetail" id="EqpMaintenancePlanDetailResult">
<result property="detailId" column="detail_id"/>
<result property="planId" column="plan_id"/>
<result property="recordId" column="record_id"/>
<result property="equipmentId" column="equipment_id"/>
<result property="productionLine" column="production_line"/>
<result property="componentName" column="component_name"/>
<result property="itemPlanDate" column="item_plan_date"/>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.klp.flow.mapper.SchProdProcessMapper">
<resultMap type="com.klp.flow.domain.SchProdProcess" id="SchProdProcessResult">
<result property="processId" column="process_id"/>
<result property="processName" column="process_name"/>
<result property="processDesc" column="process_desc"/>
<result property="remark" column="remark"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="delFlag" column="del_flag"/>
</resultMap>
</mapper>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.klp.flow.mapper.SchProdProcessStepMapper">
<resultMap type="com.klp.flow.domain.SchProdProcessStep" id="SchProdProcessStepResult">
<result property="stepId" column="step_id"/>
<result property="processId" column="process_id"/>
<result property="stepOrder" column="step_order"/>
<result property="stepName" column="step_name"/>
<result property="remark" column="remark"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="delFlag" column="del_flag"/>
</resultMap>
</mapper>

View File

@@ -8,6 +8,7 @@
<result property="scheduleDetailId" column="schedule_detail_id"/>
<result property="scheduleId" column="schedule_id"/>
<result property="orderDetailId" column="order_detail_id"/>
<result property="processId" column="process_id"/>
<result property="spec" column="spec"/>
<result property="material" column="material"/>
<result property="scheduleWeight" column="schedule_weight"/>

View File

@@ -31,8 +31,8 @@
<result property="otherTechReq" column="other_tech_req"/>
<result property="paymentDesc" column="payment_desc"/>
<result property="returnReason" column="return_reason"/>
<result property="scheduleIds" column="schedule_ids"/>
<result property="orderDetailIds" column="order_detail_ids"/>
<result property="scheduleDetailIds" column="schedule_detail_ids"/>
<result property="actionId" column="action_id"/>
<result property="spec" column="spec"/>
<result property="material" column="material"/>
<result property="scheduleWeight" column="schedule_weight"/>

View File

@@ -56,3 +56,13 @@ export function syncRecords({ starttime, endtime }) {
}
})
}
// 导出考勤记录表
export function exportAttendanceReport(params) {
return request({
url: '/wms/attendanceRecords/exportReport',
method: 'post',
params: params,
responseType: 'blob'
})
}

View File

@@ -1,26 +1,26 @@
// lockValue: 锁值用于防止并发操作不同的lockValue对应不同的锁
// 1分步加工 2. 退火 3. 酸轧 4. 酸轧分条 4. 合卷
export const PROCESSES = [
{ name: '酸连轧工序', actionType: 11, api: 'pendingAction', lockValue: 3 },
{ name: '酸轧分条工序', actionType: 120, api: 'pendingAction', lockValue: 4 },
{ name: '酸轧合卷', actionType: 201, api: 'pendingAction', lockValue: 5 },
{ name: '镀锌合卷', actionType: 202, api: 'pendingAction', lockValue: 5 },
{ name: '脱脂合卷', actionType: 203, api: 'pendingAction', lockValue: 5 },
{ name: '拉矫平整合卷', actionType: 204, api: 'pendingAction', lockValue: 5 },
{ name: '双机架合卷', actionType: 205, api: 'pendingAction', lockValue: 5 },
{ name: '镀铬合卷', actionType: 206, api: 'pendingAction', lockValue: 5 },
{ name: '镀锌工序', actionType: 501, api: 'specialSplit', lockValue: 1 },
{ name: '脱脂工序', actionType: 502, api: 'specialSplit', lockValue: 1 },
{ name: '拉矫平整工序', actionType: 503, api: 'specialSplit', lockValue: 1 },
{ name: '双机架工序', actionType: 504, api: 'specialSplit', lockValue: 1 },
{ name: '镀铬工序', actionType: 505, api: 'specialSplit', lockValue: 1 },
{ name: '纵剪分条工序', actionType: 506, api: 'specialSplit', lockValue: 1 },
{ name: '酸轧修复工序', actionType: 520, api: 'specialSplit', lockValue: 1 },
{ name: '镀锌修复工序', actionType: 521, api: 'specialSplit', lockValue: 1 },
{ name: '脱脂修复工序', actionType: 522, api: 'specialSplit', lockValue: 1 },
{ name: '拉矫修复工序', actionType: 523, api: 'specialSplit', lockValue: 1 },
{ name: '双机架修复工序', actionType: 524, api: 'specialSplit', lockValue: 1 },
{ name: '镀铬修复工序', actionType: 525, api: 'specialSplit', lockValue: 1 },
{ name: '酸连轧工序', actionId: 11, api: 'pendingAction', lockValue: 3 },
{ name: '酸轧分条工序', actionId: 120, api: 'pendingAction', lockValue: 4 },
{ name: '酸轧合卷', actionId: 201, api: 'pendingAction', lockValue: 5 },
{ name: '镀锌合卷', actionId: 202, api: 'pendingAction', lockValue: 5 },
{ name: '脱脂合卷', actionId: 203, api: 'pendingAction', lockValue: 5 },
{ name: '拉矫平整合卷', actionId: 204, api: 'pendingAction', lockValue: 5 },
{ name: '双机架合卷', actionId: 205, api: 'pendingAction', lockValue: 5 },
{ name: '镀铬合卷', actionId: 206, api: 'pendingAction', lockValue: 5 },
{ name: '镀锌工序', actionId: 501, api: 'specialSplit', lockValue: 1 },
{ name: '脱脂工序', actionId: 502, api: 'specialSplit', lockValue: 1 },
{ name: '拉矫平整工序', actionId: 503, api: 'specialSplit', lockValue: 1 },
{ name: '双机架工序', actionId: 504, api: 'specialSplit', lockValue: 1 },
{ name: '镀铬工序', actionId: 505, api: 'specialSplit', lockValue: 1 },
{ name: '纵剪分条工序', actionId: 506, api: 'specialSplit', lockValue: 1 },
{ name: '酸轧修复工序', actionId: 520, api: 'specialSplit', lockValue: 1 },
{ name: '镀锌修复工序', actionId: 521, api: 'specialSplit', lockValue: 1 },
{ name: '脱脂修复工序', actionId: 522, api: 'specialSplit', lockValue: 1 },
{ name: '拉矫修复工序', actionId: 523, api: 'specialSplit', lockValue: 1 },
{ name: '双机架修复工序', actionId: 524, api: 'specialSplit', lockValue: 1 },
{ name: '镀铬修复工序', actionId: 525, api: 'specialSplit', lockValue: 1 },
]
export const PRODUCTION_LINES = [

View File

@@ -0,0 +1,488 @@
<template>
<div class="app-container cost-trend-page">
<!-- 筛选区 -->
<el-card class="mb8">
<div class="filter-bar">
<el-radio-group
v-model="selectedLine"
size="small"
@change="onLineChange"
>
<el-radio-button
v-for="line in productionLines"
:key="line.lineId"
:label="line.lineId"
>
{{ line.lineName }}
</el-radio-button>
</el-radio-group>
<el-button type="primary" size="small" icon="el-icon-search" :loading="loading" @click="fetchData">
查询
</el-button>
</div>
</el-card>
<!-- 统计摘要 -->
<div v-if="reports.length > 0" class="stats-row">
<div class="stat-item stat-blue">
<span class="stat-val">{{ reports.length }}</span>
<span class="stat-label">参与报表数</span>
</div>
<div class="stat-item stat-orange">
<span class="stat-val">{{ summary.seriesCount }}</span>
<span class="stat-label">趋势线数</span>
</div>
<div class="stat-item stat-green">
<span class="stat-val">{{ summary.maxLabel }}</span>
<span class="stat-label">单报表最高数量</span>
</div>
</div>
<!-- 图表筛选 -->
<div v-if="reports.length > 0" class="chart-filter-bar">
<span class="filter-label">图表筛选</span>
<el-select
v-model="selectedCategories"
multiple
collapse-tags
placeholder="成本类别(全部)"
size="small"
style="width:160px"
@change="onCategoryChange"
clearable
>
<el-option label="原料" value="原料" />
<el-option label="能耗" value="能耗" />
<el-option label="辅料" value="辅料" />
<el-option label="设备" value="设备" />
<el-option label="人工" value="人工" />
<el-option label="产出" value="产出" />
<el-option label="轧辊" value="轧辊" />
<el-option label="其他" value="其他" />
</el-select>
<el-select
v-model="selectedItems"
multiple
collapse-tags
filterable
placeholder="成本项(全部)"
size="small"
style="width:220px"
@change="onItemChange"
clearable
>
<el-option
v-for="it in filteredItemOptions"
:key="it.itemId"
:label="it.itemName + ' (' + it.category + ')'"
:value="it.itemId"
/>
</el-select>
</div>
<!-- 图表区 -->
<div class="chart-box" v-loading="loading">
<div ref="trendChart" class="chart-body" />
</div>
<div v-if="summary.seriesCount > 15" class="legend-hint">
<i class="el-icon-info" /> 图例较多可在下方图例区滚动查看或缩小筛选范围以减少趋势线数量
</div>
</div>
</template>
<script>
import * as echarts from 'echarts'
import { mapGetters } from 'vuex'
import { listProdReport } from '@/api/cost/prodReport'
import { listProdDetail } from '@/api/cost/prodDetail'
import { listItem } from '@/api/cost/item'
const COLORS = [
'#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399',
'#9b59b6', '#1abc9c', '#e74c3c', '#3498db', '#2ecc71',
'#f39c12', '#e91e63', '#00bcd4', '#8bc34a', '#ff5722',
'#607d8b', '#3f51b5', '#cddc39', '#ff9800', '#795548',
'#2196f3', '#4caf50', '#ffc107', '#9c27b0', '#00bcd4'
]
export default {
name: 'CostTrend',
data() {
return {
selectedLine: null,
selectedCategories: [],
selectedItems: [],
allItems: [],
loading: false,
chart: null,
reports: [],
cachedDetails: [],
summary: { seriesCount: 0, maxLabel: '-' }
}
},
computed: {
...mapGetters(['productionLines']),
selectedLineObj() {
if (!this.selectedLine || !this.productionLines.length) return null
return this.productionLines.find(l => l.lineId === this.selectedLine) || null
},
lineNameDisplay() {
const line = this.selectedLineObj
return line ? line.lineName : ''
},
filteredItemOptions() {
if (!this.selectedCategories.length) return this.allItems
return this.allItems.filter(it => this.selectedCategories.includes(it.category))
}
},
mounted() {
this.$store.dispatch('productionLine/getProductionLines').then(() => {
this.initDefaultLine()
this.$nextTick(() => this.fetchData())
})
this.loadItems()
this._resizeHandler = () => { if (this.chart) this.chart.resize() }
window.addEventListener('resize', this._resizeHandler)
},
beforeDestroy() {
window.removeEventListener('resize', this._resizeHandler)
if (this.chart) { this.chart.dispose(); this.chart = null }
},
methods: {
initDefaultLine() {
if (this.selectedLine != null) return
const lines = this.productionLines || []
const dzLine = lines.find(l => l.lineName && l.lineName.includes('镀锌'))
if (dzLine) {
this.selectedLine = dzLine.lineId
} else if (lines.length) {
this.selectedLine = lines[0].lineId
}
},
onLineChange() {
if (this.selectedLine != null) {
this.fetchData()
}
},
onCategoryChange() {
this.selectedItems = this.selectedItems.filter(id => {
const it = this.allItems.find(item => item.itemId === id)
return it && this.selectedCategories.includes(it.category)
})
this.renderFromCache()
},
onItemChange() {
this.renderFromCache()
},
async loadItems() {
try {
const r = await listItem({ pageNum: 1, pageSize: 999 })
this.allItems = r.rows || []
} catch (e) { /* ignore */ }
},
async fetchData() {
if (this.selectedLine == null) {
return
}
this.loading = true
try {
const params = {
lineType: this.selectedLine,
pageNum: 1,
pageSize: 999
}
const reportRes = await listProdReport(params)
const allReports = (reportRes.rows || []).sort((a, b) => {
return (a.reportDate || '').localeCompare(b.reportDate || '')
})
console.log('[CostTrend] reports:', allReports.length, allReports.map(r => ({ id: r.reportId, title: r.reportTitle, date: r.reportDate })))
if (!allReports.length) {
this.$modal.msgWarning('该产线暂无报表')
this.reports = []
this.summary = { seriesCount: 0, maxLabel: '-' }
if (this.chart) this.chart.clear()
return
}
this.reports = allReports
await this.fetchDetailsAndCache(allReports)
} catch (e) {
console.error('CostTrend fetchData error:', e)
this.$modal.msgError('数据加载失败: ' + (e.message || '未知错误'))
} finally {
this.loading = false
}
},
// ==================== 成本项模式 ====================
async fetchDetailsAndCache(reports) {
const allDetailResults = await Promise.all(
reports.map(r =>
listProdDetail({ reportId: r.reportId, pageNum: 1, pageSize: 99999 })
.then(res => {
console.log('[CostTrend] detail for report', r.reportId, 'rows:', (res.rows || []).length)
return { reportId: r.reportId, rows: res.rows || [] }
})
.catch(e => {
console.error('[CostTrend] detail fetch error for report', r.reportId, e)
return { reportId: r.reportId, rows: [] }
})
)
)
this.cachedDetails = allDetailResults
if (!this.allItems.length) await this.loadItems()
this.renderFromCache()
},
renderFromCache() {
const reports = this.reports
const allDetailResults = this.cachedDetails
if (!reports.length || !allDetailResults.length) return
const itemMap = {}
this.allItems.forEach(it => { itemMap[String(it.itemId)] = it })
const itemSet = this.selectedItems.length ? new Set(this.selectedItems.map(String)) : null
const categorySet = this.selectedCategories.length ? new Set(this.selectedCategories) : null
const reportSums = {}
allDetailResults.forEach(({ reportId, rows }) => {
const sums = {}
rows.forEach(d => {
if (!d.itemId) return
const key = String(d.itemId)
if (itemSet && !itemSet.has(key)) return
if (categorySet) {
const it = itemMap[key]
if (!it || !categorySet.has(it.category)) return
}
sums[key] = (sums[key] || 0) + (parseFloat(d.quantity) || 0)
})
reportSums[reportId] = sums
})
const allItemIds = new Set()
Object.values(reportSums).forEach(sums => {
Object.keys(sums).forEach(id => allItemIds.add(id))
})
const xLabels = reports.map(r => this.reportLabel(r))
const series = []
let globalMax = 0
Array.from(allItemIds).forEach((key, idx) => {
const it = itemMap[key]
const name = it ? it.itemName : `未知(${key})`
const data = reports.map(r => {
const sums = reportSums[r.reportId] || {}
const val = sums[key] ?? null
if (val != null && val > globalMax) globalMax = val
return val
})
if (data.every(v => v === null)) return
series.push({
name,
type: 'line',
data,
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: { width: 2, color: COLORS[idx % COLORS.length] },
itemStyle: { color: COLORS[idx % COLORS.length] },
emphasis: { focus: 'series' }
})
})
this.summary = {
seriesCount: series.length,
maxLabel: this.formatMoney(globalMax)
}
this.$nextTick(() => {
setTimeout(() => this.renderChart(xLabels, series, '数量'), 50)
})
},
// ==================== 图表渲染 ====================
renderChart(xLabels, series, yAxisName) {
console.log('[CostTrend] renderChart called, ref:', !!this.$refs.trendChart, 'series:', series.length, 'xLabels:', xLabels.length)
if (!this.$refs.trendChart) return
if (this.chart) this.chart.dispose()
this.chart = echarts.init(this.$refs.trendChart)
if (!series.length) {
this.chart.setOption({
title: { text: '暂无数据', left: 'center', top: 'center', textStyle: { color: '#999', fontSize: 14 } },
xAxis: { show: false },
yAxis: { show: false },
series: []
})
return
}
this.chart.setOption({
tooltip: {
trigger: 'axis',
formatter: function (params) {
if (!params || !params.length) return ''
let html = '<b>' + params[0].axisValue + '</b><br/>'
params.forEach(p => {
if (p.value != null) {
html += '<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:' +
p.color + ';margin-right:5px;"></span>' +
p.seriesName + ': <b>' +
Number(p.value).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) +
'</b><br/>'
}
})
return html
}
},
legend: {
type: 'scroll',
bottom: 0,
textStyle: { fontSize: 11 },
pageIconSize: 12,
pageTextStyle: { fontSize: 11 }
},
grid: {
left: '3%',
right: '4%',
top: '3%',
bottom: series.length > 20 ? '18%' : (series.length > 10 ? '14%' : '10%'),
containLabel: true
},
xAxis: {
type: 'category',
data: xLabels,
boundaryGap: false,
axisLabel: {
fontSize: 11,
rotate: xLabels.length > 8 ? 30 : 0
}
},
yAxis: {
type: 'value',
name: yAxisName,
axisLabel: {
fontSize: 11,
formatter: function (v) {
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
return v
}
},
splitLine: { lineStyle: { type: 'dashed', color: '#e8e8e8' } }
},
dataZoom: xLabels.length > 10 ? [
{
type: 'slider',
bottom: series.length > 10 ? (series.length > 20 ? 60 : 50) : 35,
height: 16,
textStyle: { fontSize: 10 }
},
{ type: 'inside' }
] : [],
series
}, true)
},
reportLabel(r) {
const date = r.reportDate
? (typeof r.reportDate === 'string' ? r.reportDate.slice(0, 10) : String(r.reportDate).slice(0, 10))
: ''
return r.reportTitle || date || `报表#${r.reportId}`
},
formatMoney(val) {
if (val == null || val === '' || isNaN(val)) return '-'
const num = Number(val)
if (num >= 10000) return (num / 10000).toFixed(2) + '万'
return num.toFixed(2)
}
}
}
</script>
<style scoped>
.mb8 { margin-bottom: 10px; }
.filter-bar {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.stats-row {
display: flex;
gap: 8px;
margin-bottom: 10px;
}
.stat-item {
flex: 1;
min-width: 100px;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 2px;
padding: 10px 12px;
text-align: center;
}
.stat-val {
display: block;
font-size: 22px;
font-weight: 600;
color: #303133;
line-height: 1.2;
}
.stat-label {
display: block;
font-size: 12px;
color: #909399;
margin-top: 2px;
}
.stat-blue .stat-val { color: #409eff; }
.stat-orange .stat-val { color: #e6a23c; }
.stat-green .stat-val { color: #67c23a; }
.chart-box {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 2px;
}
.chart-filter-bar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.chart-filter-bar .filter-label {
font-size: 13px;
color: #606266;
white-space: nowrap;
}
.chart-body {
width: 100%;
height: 480px;
}
.legend-hint {
margin-top: 8px;
font-size: 12px;
color: #909399;
display: flex;
align-items: center;
gap: 4px;
}
</style>

View File

@@ -663,8 +663,17 @@ export default {
if (v == null || rv == null || rv === 0) return null
return parseFloat(v.toFixed(3))
})
const chartTitle = title
// 设定值(绝对厚度 mm取首个非空 REF写入标题
let refVal = null
for (const r of rows) {
const rv = getRowVal(r, refCol)
if (rv != null && rv !== 0) { refVal = rv; break }
}
const chartTitle = refVal != null ? `${title} - 设定值: ${refVal.toFixed(3)}[mm]` : title
const extras = []
// 设定基准线恒为 0
extras.push({ name: '设定值(0)', data: yData.map(v => v == null ? null : 0), color: '#909399', dash: false })
// TOPLIMIT/BOTLIMIT 是冷轧机出口厚度的 AGC 公差带(逐行变化,
// 头尾加减速段约 ±2.5%、稳态段收紧到约 ±1%),只约束出口厚度。

View File

@@ -231,15 +231,55 @@
</span>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType != 'F'">
<el-col :span="24" v-if="form.menuType != 'F'">
<el-form-item prop="style">
<el-input v-model="form.style" placeholder="请输入菜单样式" maxlength="255" />
<span slot="label">
<el-tooltip content='菜单样式JSON格式如`{"backgroundColor":"#ff0000","color":"#fff","fontWeight":"bold"}`' placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
菜单样式
</span>
<div class="style-editor">
<el-input
type="textarea"
v-model="form.style"
placeholder='请输入菜单样式JSON格式'
:rows="4"
maxlength="500"
show-word-limit
class="style-textarea"
/>
<div class="style-toolbar">
<div class="style-presets">
<span class="preset-label">快捷样式</span>
<el-button size="mini" @click="applyStylePreset('danger')">浅红</el-button>
<el-button size="mini" @click="applyStylePreset('success')">浅绿</el-button>
<el-button size="mini" @click="applyStylePreset('warning')">浅橙</el-button>
<el-button size="mini" @click="applyStylePreset('primary')">浅蓝</el-button>
<el-button size="mini" @click="applyStylePreset('accent')">浅紫</el-button>
<el-button size="mini" @click="applyStylePreset('warm')">暖色</el-button>
<el-button size="mini" @click="applyStylePreset('bold')">加粗</el-button>
<el-button size="mini" @click="applyStylePreset('italic')">斜体</el-button>
<el-button size="mini" @click="applyStylePreset('clear')">清除</el-button>
</div>
<div class="style-actions">
<el-button size="mini" type="success" icon="el-icon-check" @click="validateStyle">校验</el-button>
<el-button size="mini" type="info" icon="el-icon-view" @click="showPreview = !showPreview">{{ showPreview ? '隐藏预览' : '预览样式' }}</el-button>
</div>
</div>
<div v-if="styleError" class="style-error">
<i class="el-icon-warning"></i> {{ styleError }}
</div>
<div v-if="showPreview && form.style" class="style-preview">
<div class="preview-title">菜单预览效果</div>
<div class="preview-container">
<div class="preview-menu-item" :style="parsedStyle">
<span class="preview-icon">📁</span>
<span class="preview-text">{{ form.menuName || '菜单名称' }}</span>
</div>
</div>
</div>
</div>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType == 'C'">
@@ -311,6 +351,16 @@ export default {
name: "Menu",
dicts: ['sys_show_hide', 'sys_normal_disable'],
components: { Treeselect, IconSelect },
computed: {
parsedStyle() {
if (!this.form.style) return {};
try {
return JSON.parse(this.form.style);
} catch (e) {
return {};
}
}
},
data() {
return {
// 遮罩层
@@ -334,6 +384,10 @@ export default {
menuName: undefined,
visible: undefined
},
// 菜单样式预览
showPreview: false,
// 菜单样式校验错误
styleError: '',
// 表单参数
form: {},
// 表单校验
@@ -358,6 +412,67 @@ export default {
selected(name) {
this.form.icon = name;
},
/** 应用样式预设 */
applyStylePreset(type) {
const presets = {
danger: { background: 'linear-gradient(135deg, #fff5f5 0%, #ffe0e0 100%)', color: '#c45c5c', borderRadius: '4px', border: '1px solid #ffd4d4' },
success: { background: 'linear-gradient(135deg, #f0fff4 0%, #dcffe4 100%)', color: '#5c8a6b', borderRadius: '4px', border: '1px solid #c3e6cb' },
warning: { background: 'linear-gradient(135deg, #fffdf0 0%, #fff3cd 100%)', color: '#b8860b', borderRadius: '4px', border: '1px solid #ffeeba' },
primary: { background: 'linear-gradient(135deg, #f0f7ff 0%, #d6eaff 100%)', color: '#4a7fb5', borderRadius: '4px', border: '1px solid #b8daff' },
accent: { background: 'linear-gradient(135deg, #f8f0ff 0%, #e8d5ff 100%)', color: '#8b5cf6', borderRadius: '4px', border: '1px solid #d8b4fe' },
warm: { background: 'linear-gradient(135deg, #fff8f0 0%, #ffedd5 100%)', color: '#c27832', borderRadius: '4px', border: '1px solid #fed7aa' },
bold: { fontWeight: '600', color: '#303133' },
italic: { fontStyle: 'italic', color: '#606266' },
clear: {}
};
if (type === 'clear') {
this.form.style = '';
this.styleError = '';
} else {
// 合并已有样式
let currentStyle = {};
if (this.form.style) {
try {
currentStyle = JSON.parse(this.form.style);
} catch (e) {
currentStyle = {};
}
}
const mergedStyle = { ...currentStyle, ...presets[type] };
this.form.style = JSON.stringify(mergedStyle);
this.validateStyle();
}
},
/** 校验菜单样式JSON格式 */
validateStyle() {
this.styleError = '';
if (!this.form.style) {
this.$modal.msgSuccess('样式为空,无需校验');
return;
}
try {
const styleObj = JSON.parse(this.form.style);
if (typeof styleObj !== 'object' || styleObj === null) {
this.styleError = '样式必须是JSON对象格式';
this.$modal.msgError('校验失败样式必须是JSON对象格式');
return;
}
// 检查是否包含有效的CSS属性
const validCssProps = [
'backgroundColor', 'background', 'color', 'fontWeight', 'fontSize', 'fontStyle',
'borderRadius', 'padding', 'margin', 'border', 'textAlign',
'textDecoration', 'lineHeight', 'letterSpacing', 'opacity', 'boxShadow'
];
const invalidProps = Object.keys(styleObj).filter(prop => !validCssProps.includes(prop));
if (invalidProps.length > 0) {
this.styleError = `可能无效的属性:${invalidProps.join(', ')}`;
}
this.$modal.msgSuccess('校验通过JSON格式正确');
} catch (e) {
this.styleError = `JSON格式错误${e.message}`;
this.$modal.msgError('校验失败:' + e.message);
}
},
/** 查询菜单列表 */
getList() {
this.loading = true;
@@ -554,3 +669,107 @@ export default {
}
};
</script>
<style lang="scss" scoped>
.style-editor {
width: 100%;
max-width: 600px;
.style-textarea {
margin-bottom: 8px;
:deep(.el-textarea__inner) {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
line-height: 1.5;
padding: 8px;
}
}
.style-toolbar {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 8px;
.style-presets {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px;
.preset-label {
font-size: 12px;
color: #909399;
margin-right: 4px;
}
.el-button {
margin-left: 0;
}
}
.style-actions {
display: flex;
gap: 4px;
}
}
.style-error {
color: #F56C6C;
font-size: 12px;
margin-bottom: 8px;
padding: 4px 8px;
background-color: #FEF0F0;
border-radius: 4px;
border: 1px solid #FBC4C4;
i {
margin-right: 4px;
}
}
.style-preview {
border: 1px solid #EBEEF5;
border-radius: 4px;
padding: 12px;
background-color: #FAFAFA;
.preview-title {
font-size: 12px;
color: #909399;
margin-bottom: 8px;
}
.preview-container {
display: flex;
justify-content: center;
padding: 16px;
background-color: #F5F7FA;
border-radius: 4px;
.preview-menu-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background-color: #fff;
border-radius: 4px;
min-width: 120px;
justify-content: center;
transition: all 0.3s ease;
.preview-icon {
font-size: 16px;
}
.preview-text {
font-size: 14px;
}
}
}
}
}
</style>

View File

@@ -20,7 +20,7 @@
</el-form-item>
<!-- 查找并选择选择收货计划, 使用远程搜索 -->
<el-alert v-if="noPlan" type="warning" title="今天还没有收货计划,点击创建" show-icon
@click.native="$router.push('/receive/plan')"></el-alert>
@click.native="$router.push('/wip/receive/plan')"></el-alert>
</el-col>
</el-row>
<el-row>

View File

@@ -598,6 +598,14 @@
text-shadow: 0 0 6px rgba(100, 116, 139, 0.4) !important;
}
.attr-arrow {
display: inline-block;
margin: 0 4px;
color: #94a3b8;
font-size: 11px;
font-weight: 400;
}
.coil-futuristic-footer {
display: flex;
justify-content: space-around;

View File

@@ -5,7 +5,7 @@
<span class="section-title">基本信息</span>
</div>
<div class="section-body">
<CoilInfoRender :coilInfo="coilInfo" :column="5" />
<CoilInfoRender :coilInfo="displayCoilInfo" :column="5" />
</div>
</div>
</template>
@@ -14,7 +14,20 @@
export default {
name: 'BasicInfoSection',
props: {
coilInfo: { type: Object, default: () => ({}) }
coilInfo: { type: Object, default: () => ({}) },
initialQualityStatus: { type: String, default: '' }
},
computed: {
displayCoilInfo() {
const currentStatus = this.coilInfo.qualityStatus || '-'
if (this.initialQualityStatus && this.initialQualityStatus !== currentStatus) {
return {
...this.coilInfo,
qualityStatus: `${this.initialQualityStatus}${currentStatus}`
}
}
return this.coilInfo
}
}
}
</script>

View File

@@ -55,7 +55,11 @@
<div class="attr-tag attr-left">
<div class="attr-content">
<span class="attr-label">{{ leftLabel }}</span>
<span class="attr-value" :class="statusClass(coil.qualityStatus)">{{ coil.qualityStatus || '-' }}</span>
<span class="attr-value" :class="statusClass(initialQualityStatus)">{{ initialQualityStatus || '-' }}</span>
<template v-if="hasQualityChanged">
<span class="attr-arrow"></span>
<span class="attr-value" :class="statusClass(currentQualityStatus)">{{ currentQualityStatus || '-' }}</span>
</template>
</div>
</div>
<div v-if="coil.enterCoilNo && coil.enterCoilNo !== '-'" class="attr-tag attr-top-left">
@@ -116,7 +120,8 @@ export default {
name: 'CoilCardFuturistic',
props: {
coil: { type: Object, default: () => ({}) },
type: { type: String, default: 'inbound' }
type: { type: String, default: 'inbound' },
initialQualityStatus: { type: String, default: '' }
},
data() {
uid++
@@ -142,6 +147,13 @@ export default {
},
leftLabel() {
return this.type === 'inbound' ? '质量状态' : '状态'
},
currentQualityStatus() {
return this.coil.qualityStatus || '-'
},
hasQualityChanged() {
return this.initialQualityStatus && this.currentQualityStatus !== '-'
&& this.initialQualityStatus !== this.currentQualityStatus
}
},
methods: {

View File

@@ -38,6 +38,7 @@
<span class="card-label">&#128230; 入库</span>
</div>
<CoilCardFuturistic v-for="(coil, idx) in getCreationCoils(step, allSteps)" :key="idx" :coil="coil" type="inbound"
:initialQualityStatus="initialQualityStatus"
v-if="coil && coil.currentCoilNo !== '-'" />
<div v-if="isEmpty(getCreationCoils(step, allSteps))" class="empty-coil">
<span>无钢卷信息</span>
@@ -147,7 +148,8 @@ export default {
components: { CoilTraceResult, CoilCardFuturistic, CoilCardCompact, ShipmentCard },
props: {
traceResult: { type: Object, default: null },
coilInfo: { type: Object, default: () => ({}) }
coilInfo: { type: Object, default: () => ({}) },
initialQualityStatus: { type: String, default: '' }
},
methods: {
isEmpty(list) {

View File

@@ -1,12 +1,12 @@
<template>
<div class="coil-info-page" v-loading="loading">
<div class="content-container">
<BasicInfoSection :coilInfo="coilInfo" />
<BasicInfoSection :coilInfo="coilInfo" :initialQualityStatus="initialQualityStatus" />
<CostInfoSection :coilInfo="coilInfo" :traceResult="traceResult"
:hoardingDays="hoardingDays" :hoardingCost="hoardingCost" />
<LifecycleTrace :traceResult="traceResult" :coilInfo="coilInfo" />
<LifecycleTrace :traceResult="traceResult" :coilInfo="coilInfo" :initialQualityStatus="initialQualityStatus" />
<ContractInfo title="生产合同信息" :info="salesInfo" emptyText="未找到相关生产合同信息" />
@@ -135,6 +135,17 @@ export default {
hoardingCost() {
const netWeight = parseFloat(this.coilInfo.netWeight) || 0
return (this.hoardingDays * netWeight).toFixed(2)
},
initialQualityStatus() {
if (!this.coilQualityRejudgeList || this.coilQualityRejudgeList.length === 0) {
return this.coilInfo.qualityStatus || '-'
}
const sorted = [...this.coilQualityRejudgeList].sort((a, b) => {
const timeA = new Date(a.createTime || 0).getTime()
const timeB = new Date(b.createTime || 0).getTime()
return timeA - timeB
})
return sorted[0].beforeQuality || this.coilInfo.qualityStatus || '-'
}
},
async created() {

View File

@@ -69,6 +69,7 @@
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
<el-button type="warning" icon="el-icon-upload" size="mini" @click="handleSync">同步</el-button>
<el-button type="success" icon="el-icon-download" size="mini" @click="handleExportReport">导出考勤表</el-button>
</el-form-item>
</el-form>
@@ -187,11 +188,42 @@
<el-button @click="syncDialogVisible = false">取消</el-button>
</div>
</el-dialog>
<!-- 导出考勤表对话框 -->
<el-dialog title="导出考勤记录表" :visible.sync="exportDialogVisible" width="480px" append-to-body>
<el-form ref="exportForm" :model="exportFormData" :rules="exportRules" label-width="90px">
<el-form-item label="考勤时间" prop="dateRange">
<el-date-picker
v-model="exportFormData.dateRange"
type="daterange"
range-separator="~"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
:picker-options="pickerOptions"
style="width: 100%">
</el-date-picker>
</el-form-item>
<el-form-item label="员工编号" prop="pin">
<el-input v-model="exportFormData.pin" placeholder="请输入员工编号(可选)" clearable />
</el-form-item>
<el-form-item label="姓名" prop="ename">
<el-input v-model="exportFormData.ename" placeholder="请输入姓名(可选)" clearable />
</el-form-item>
<el-form-item label="部门" prop="deptname">
<el-input v-model="exportFormData.deptname" placeholder="请输入部门(可选)" clearable />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="exportLoading" type="primary" @click="doExportReport">确定导出</el-button>
<el-button @click="exportDialogVisible = false">取消</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listRecords, getRecords, delRecords, addRecords, updateRecords, syncRecords } from "@/api/wms/attendance";
import { listRecords, getRecords, delRecords, addRecords, updateRecords, syncRecords, exportAttendanceReport } from "@/api/wms/attendance";
export default {
name: "Records",
@@ -211,6 +243,41 @@ export default {
syncRules: {
syncMonth: [{ required: true, message: "请选择同步月份", trigger: "change" }]
},
// 导出弹窗
exportDialogVisible: false,
// 导出loading
exportLoading: false,
// 导出表单
exportFormData: {
dateRange: [],
pin: "",
ename: "",
deptname: ""
},
// 导出表单校验
exportRules: {
dateRange: [{ required: true, message: "请选择考勤时间范围", trigger: "change" }]
},
// 日期选择器快捷选项
pickerOptions: {
shortcuts: [{
text: '本月',
onClick(picker) {
const now = new Date();
const start = new Date(now.getFullYear(), now.getMonth(), 1);
const end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
picker.$emit('pick', [start, end]);
}
}, {
text: '上月',
onClick(picker) {
const now = new Date();
const start = new Date(now.getFullYear(), now.getMonth() - 1, 1);
const end = new Date(now.getFullYear(), now.getMonth(), 0);
picker.$emit('pick', [start, end]);
}
}]
},
// 遮罩层
loading: true,
// 选中数组
@@ -391,6 +458,61 @@ export default {
this.download('wms/records/export', {
...this.queryParams
}, `records_${new Date().getTime()}.xlsx`)
},
/** 导出考勤记录表 */
handleExportReport() {
this.$modal.confirm(
'导出前请先确保已同步最新的考勤数据,是否继续导出?',
'提示',
{
confirmButtonText: '已同步,继续导出',
cancelButtonText: '去同步',
type: 'info'
}
).then(() => {
this.exportFormData.dateRange = [];
this.exportFormData.pin = "";
this.exportFormData.ename = "";
this.exportFormData.deptname = "";
this.exportDialogVisible = true;
this.$nextTick(() => {
this.$refs.exportForm && this.$refs.exportForm.clearValidate();
});
}).catch(action => {
if (action === 'cancel') {
this.handleSync();
}
});
},
/** 执行导出考勤记录表 */
doExportReport() {
this.$refs.exportForm.validate(valid => {
if (!valid) return;
const [startDate, endDate] = this.exportFormData.dateRange;
this.exportLoading = true;
exportAttendanceReport({
startTime: startDate,
endTime: endDate,
pin: this.exportFormData.pin || undefined,
ename: this.exportFormData.ename || undefined,
deptname: this.exportFormData.deptname || undefined
}).then(res => {
const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `考勤记录表_${startDate}_${endDate}.xlsx`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
this.$modal.msgSuccess("导出成功");
this.exportLoading = false;
this.exportDialogVisible = false;
}).catch(() => {
this.exportLoading = false;
});
});
}
}
};

View File

@@ -205,9 +205,9 @@
<el-table-column type="selection" width="45" align="center" />
<el-table-column label="排产单号" prop="scheduleNo" min-width="140" show-overflow-tooltip />
<el-table-column label="生产日期" prop="prodDate" width="110" align="center" show-overflow-tooltip />
<el-table-column label="工序类型" prop="actionType" width="100" align="center" show-overflow-tooltip>
<el-table-column label="工序类型" prop="actionId" width="100" align="center" show-overflow-tooltip>
<template slot-scope="scope">
{{ getActionTypeName(scope.row.actionType) }}
{{ getActionIdName(scope.row.actionId) }}
</template>
</el-table-column>
<el-table-column label="排产状态" prop="scheduleStatus" width="90" align="center">
@@ -317,8 +317,8 @@
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="工序类型">
<el-select v-model="editForm.actionType" placeholder="请选择工序类型" clearable filterable style="width:100%">
<el-option v-for="p in processOptions" :key="p.actionType" :label="p.name" :value="p.actionType" />
<el-select v-model="editForm.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>
@@ -528,8 +528,8 @@
</el-col>
<el-col :span="12">
<el-form-item label="工序类型">
<el-select v-model="mergeForm.actionType" placeholder="请选择工序类型" clearable filterable style="width:100%">
<el-option v-for="p in processOptions" :key="p.actionType" :label="p.name" :value="p.actionType" />
<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>
@@ -687,7 +687,7 @@ export default {
mergeTemplateIndex: 0,
mergeSourceRows: [],
mergeForm: {
itemCount: 0, scheduleNo: '', actionType: '', customerName: '', spec: '', material: '',
itemCount: 0, scheduleNo: '', actionId: '', customerName: '', spec: '', material: '',
scheduleWeight: 0, productType: '', productItem: '', businessUser: '',
businessPhone: '', deliveryCycle: undefined, usePurpose: '',
thicknessTolerance: '', widthTolerance: '', surfaceQuality: '',
@@ -730,7 +730,7 @@ export default {
return {
scheduleId: undefined,
scheduleNo: '',
actionType: '',
actionId: '',
prodDate: '',
scheduleStatus: undefined,
totalPlanWeight: undefined,
@@ -891,9 +891,9 @@ export default {
handleEditScheduled(row) {
this.editForm = { ...this.getEmptyEditForm(), ...row }
// 确保 actionType 为数字类型以匹配下拉选项
if (this.editForm.actionType != null && typeof this.editForm.actionType !== 'number') {
this.editForm.actionType = Number(this.editForm.actionType)
// 确保 actionId 为数字类型以匹配下拉选项
if (this.editForm.actionId != null && typeof this.editForm.actionId !== 'number') {
this.editForm.actionId = Number(this.editForm.actionId)
}
// 确保 scheduleStatus 为数字类型以匹配下拉选项
if (this.editForm.scheduleStatus != null && typeof this.editForm.scheduleStatus !== 'number') {
@@ -977,7 +977,7 @@ export default {
this.mergeForm = {
itemCount: this.mergeSourceRows.length,
scheduleNo: row.scheduleNo || '',
actionType: row.actionType != null ? Number(row.actionType) : '',
actionId: row.actionId != null ? Number(row.actionId) : '',
customerName: row.customerName || '',
spec: row.spec || '',
material: row.material || '',
@@ -1033,9 +1033,9 @@ export default {
return total.toFixed(3)
},
getActionTypeName(actionType) {
const p = this.processOptions.find(item => String(item.actionType) === String(actionType))
return p ? p.name : (actionType || '')
getActionIdName(actionId) {
const p = this.processOptions.find(item => String(item.actionId) === String(actionId))
return p ? p.name : (actionId || '')
},
handleDetailClick(sch, detail) {

View File

@@ -6,13 +6,9 @@
<div class="panel-header">
<div class="header-title">
<i class="el-icon-s-tools"></i>
<span>维修计划</span>
<span>检修单</span>
<el-button size="mini" type="text" icon="el-icon-refresh" @click="getList" style="margin-left:4px;" title="刷新列表"></el-button>
</div>
<el-select v-model="queryParams.approvalStatus" placeholder="审批状态" clearable size="mini" @change="handleQuery" class="header-filter">
<el-option label="草稿" :value="0" />
<el-option label="已驳回" :value="3" />
</el-select>
</div>
<div class="search-row">
@@ -30,10 +26,6 @@
<span class="item-title">{{ item.planNo }}</span>
<span class="item-sub">{{ item.planName }}</span>
</div>
<div class="item-meta">
<el-tag v-if="item.approvalStatus === 0" type="info" size="mini">草稿</el-tag>
<el-tag v-else-if="item.approvalStatus === 3" type="danger" size="mini">已驳回</el-tag>
</div>
<div class="item-actions">
<el-button size="mini" type="text" icon="el-icon-edit" @click.stop="handleUpdate(item)"></el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click.stop="handleDelete(item)"></el-button>
@@ -41,7 +33,7 @@
</div>
<div v-if="dataList.length === 0 && !loading" class="list-empty">
<i class="el-icon-folder-opened"></i>
<span>暂无维修计划数据</span>
<span>暂无检修单数据</span>
</div>
</div>
@@ -56,84 +48,73 @@
<div class="right-panel">
<div v-if="!currentRow" class="empty-tip">
<i class="el-icon-info"></i>
<span>请在左侧列表中选择一条维修计划查看详情</span>
<span>请在左侧列表中选择一条检修单查看详情</span>
</div>
<div v-else v-loading="detailLoading" class="detail-content">
<div class="doc-header">
<div class="doc-header-top">
<div class="doc-title-group">
<div class="doc-title">{{ currentRow.planNo }}</div>
<div class="doc-subtitle">Maintenance Plan</div>
<div v-else class="right-content">
<!-- 基本信息栏 -->
<div class="info-bar">
<div class="info-bar-left">
<span class="info-label">产线</span>
<span class="info-value">{{ currentRow.productionLine || '未设置' }}</span>
<el-divider direction="vertical" />
<span class="info-label">时间</span>
<span class="info-value">{{ parseTime(currentRow.plannedStartTime, '{y}-{m}-{d}') }} ~ {{ parseTime(currentRow.plannedEndTime, '{y}-{m}-{d}') }}</span>
</div>
<div class="doc-header-right">
<el-button size="mini" type="text" icon="el-icon-refresh" @click="handleRefreshDetail" title="刷新详情">刷新</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(currentRow)" v-if="currentRow.approvalStatus !== 2">编辑</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(currentRow)" v-if="currentRow.approvalStatus !== 2">删除</el-button>
</div>
</div>
<div class="doc-status-row">
<span class="doc-status-label">Approval / 状态</span>
<el-tag v-if="currentRow.approvalStatus === 0" type="info" size="small">草稿</el-tag>
<el-tag v-else-if="currentRow.approvalStatus === 1" size="small">待审批</el-tag>
<el-tag v-else-if="currentRow.approvalStatus === 2" type="success" size="small">已审批</el-tag>
<el-tag v-else-if="currentRow.approvalStatus === 3" type="danger" size="small">已驳回</el-tag>
<div class="info-bar-right">
<el-button size="mini" icon="el-icon-search" @click="queryInspectionRecords" :disabled="!currentRow.productionLine">查询检修记录</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(currentRow)">编辑</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(currentRow)">删除</el-button>
</div>
</div>
<div class="detail-meta">
<span><i class="el-icon-document"></i>{{ currentRow.planName }}</span>
<span v-if="currentRow.repairType === 1">定期保养</span>
<span v-else-if="currentRow.repairType === 2">安全整改</span>
<span v-else-if="currentRow.repairType === 3">专项检修</span>
<span v-else-if="currentRow.repairType === 4">故障维修</span>
<span v-if="currentRow.priorityLevel === 1"><el-tag size="mini" type="info">普通</el-tag></span>
<span v-if="currentRow.priorityLevel === 2"><el-tag size="mini" type="danger">重要</el-tag></span>
<span v-if="currentRow.plannedStartTime"><i class="el-icon-time"></i>开始: {{ parseTime(currentRow.plannedStartTime, '{y}-{m}-{d}') }}</span>
<span v-if="currentRow.plannedEndTime"><i class="el-icon-time"></i>结束: {{ parseTime(currentRow.plannedEndTime, '{y}-{m}-{d}') }}</span>
<span v-if="currentRow.dutyDept"><i class="el-icon-s-home"></i>{{ currentRow.dutyDept }}</span>
<span v-if="currentRow.planOwner"><i class="el-icon-user-solid"></i>{{ currentRow.planOwner }}</span>
<span v-if="currentRow.budgetAmount"><i class="el-icon-money"></i>预算: ¥{{ currentRow.budgetAmount }}</span>
<!-- 上半部分检修记录 -->
<div class="section inspection-section">
<div class="section-header">
<span class="section-title">检修记录 <span class="en-sub">· Inspection Records</span></span>
<span class="section-count" v-if="inspectionRecords.length"> {{ inspectionTotal }} </span>
</div>
<el-divider />
<div class="section-title">
<span>计划说明 <span class="en-sub">· Description</span></span>
</div>
<div class="remark-content">{{ currentRow.planDescription || '无' }}</div>
<el-divider />
<div class="section-title">
<span>关联异常记录 <span class="en-sub">· Related Abnormal Records</span></span>
<el-button v-if="currentRow.approvalStatus === 0 || currentRow.approvalStatus === 3" size="mini" type="primary" plain icon="el-icon-plus" style="margin-left:8px;" @click="handleSelectAbnormalRecords">选择记录</el-button>
</div>
<el-table :data="abnormalList" border size="small" style="width:100%" v-loading="abnormalLoading">
<el-table-column label="设备部件" align="center" prop="partName" width="140" />
<el-table-column label="产线" align="center" prop="productionLine" width="120" />
<el-table-column label="班次" align="center" width="80">
<el-table :data="inspectionRecords" border size="small" v-loading="inspectionLoading" max-height="300"
@selection-change="handleInspectionSelectionChange" ref="inspectionTable">
<el-table-column type="selection" width="40" />
<el-table-column label="设备部件" align="center" prop="partName" min-width="120" show-overflow-tooltip />
<el-table-column label="班次" align="center" width="70">
<template slot-scope="scope">{{ scope.row.shift == 1 ? '白班' : '夜班' }}</template>
</el-table-column>
<el-table-column label="巡检时间" align="center" width="150">
<template slot-scope="scope">{{ parseTime(scope.row.inspectTime, '{y}-{m}-{d} {h}:{i}') }}</template>
</el-table-column>
<el-table-column label="异常描述" align="center" prop="abnormalDesc" min-width="160" show-overflow-tooltip />
<el-table-column label="操作" align="center" width="70" v-if="currentRow.approvalStatus === 0 || currentRow.approvalStatus === 3">
<el-table-column label="运行状态" align="center" width="80">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleRemoveAbnormal(scope.row)"></el-button>
<el-tag v-if="scope.row.runStatus == 1" type="success" size="mini">正常</el-tag>
<el-tag v-else type="danger" size="mini">故障</el-tag>
</template>
</el-table-column>
<el-table-column label="巡检人" align="center" prop="inspector" width="90" />
<el-table-column label="异常描述" align="center" prop="abnormalDesc" min-width="150" show-overflow-tooltip />
<el-table-column label="备注" align="center" prop="remark" min-width="120" show-overflow-tooltip />
</el-table>
<div v-if="abnormalList.length === 0 && !abnormalLoading" class="empty-data" style="margin-top:8px;">暂无关联异常记录</div>
<div class="section-gap" />
<div class="section-title">
<span>维修明细 <span class="en-sub">· Maintenance Details</span></span>
<el-button v-if="currentRow.approvalStatus === 0 || currentRow.approvalStatus === 3" size="mini" type="primary" plain icon="el-icon-plus" style="margin-left:8px;" @click="handleAddDetail">添加</el-button>
<div v-if="!inspectionLoading && inspectionQueried && inspectionRecords.length === 0" class="empty-data">
该产线该时间段内暂无检修记录
</div>
<el-table :data="detailList" border size="small" style="width:100%" v-loading="detailLoading">
<el-table-column label="设备部件" align="center" prop="componentName" width="130" />
<div v-if="!inspectionQueried" class="empty-data">
点击"查询检修记录"查看该产线时间段内的巡检数据
</div>
</div>
<!-- 下半部分维修明细 -->
<div class="section detail-section">
<div class="section-header">
<span class="section-title">维修明细 <span class="en-sub">· Maintenance Details</span></span>
<div class="section-actions">
<el-button size="mini" type="primary" plain icon="el-icon-plus" @click="handleAddDetail">新增</el-button>
<el-button v-if="selectedInspectionRows.length"
size="mini" type="success" plain icon="el-icon-plus" @click="addDetailFromSelection">
从选中记录添加{{ selectedInspectionRows.length }}
</el-button>
</div>
</div>
<el-table :data="detailList" border size="small" v-loading="detailLoading" max-height="300">
<el-table-column label="设备部件" align="center" prop="componentName" min-width="120" show-overflow-tooltip />
<el-table-column label="产线" align="center" prop="productionLine" width="110" />
<el-table-column label="类型" align="center" width="70">
<template slot-scope="scope">
@@ -147,31 +128,38 @@
<template slot-scope="scope">{{ parseTime(scope.row.itemPlanDate, '{y}-{m}-{d}') }}</template>
</el-table-column>
<el-table-column label="目标厂家" align="center" prop="targetManufacturer" width="110" />
<el-table-column label="操作" align="center" width="100" fixed="right" v-if="currentRow.approvalStatus === 0 || currentRow.approvalStatus === 3">
<el-table-column label="操作" align="center" width="100" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEditDetail(scope.row)"></el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDeleteDetail(scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
<div v-if="detailList.length === 0 && !detailLoading" class="empty-data" style="margin-top:8px;">暂无维修明细关联异常记录后自动生成或手动添加</div>
<div class="section-gap" />
<div v-if="currentRow.approvalStatus === 0 || currentRow.approvalStatus === 3" class="form-actions">
<el-button type="primary" :loading="submitLoading" icon="el-icon-s-promotion" @click="handleSubmitApproval">提交审批</el-button>
<div v-if="detailList.length === 0 && !detailLoading" class="empty-data">暂无维修明细请手动添加</div>
</div>
</div>
</div>
</template>
</DragResizePanel>
<!-- 新增/编辑计划弹窗 -->
<el-dialog :title="title" :visible.sync="open" width="650px" append-to-body>
<el-dialog :title="title" :visible.sync="open" width="580px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="110px">
<el-form-item label="计划名称" prop="planName">
<el-input v-model="form.planName" placeholder="请输入计划名称" />
</el-form-item>
<el-form-item label="产线" prop="productionLine">
<el-select v-model="form.productionLine" placeholder="请选择产线" clearable style="width:100%">
<el-option v-for="item in lineList" :key="item.lineId" :label="item.lineName" :value="item.lineName" />
</el-select>
</el-form-item>
<el-form-item label="开始时间" prop="plannedStartTime">
<el-date-picker clearable v-model="form.plannedStartTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="请选择" style="width:100%" />
</el-form-item>
<el-form-item label="结束时间" prop="plannedEndTime">
<el-date-picker clearable v-model="form.plannedEndTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="请选择" style="width:100%" />
</el-form-item>
<el-form-item label="维修类型" prop="repairType">
<el-select v-model="form.repairType" placeholder="请选择" style="width:100%">
<el-option label="定期保养" :value="1" />
@@ -186,24 +174,18 @@
<el-option label="重要" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="计划开始时间" prop="plannedStartTime">
<el-date-picker clearable v-model="form.plannedStartTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="请选择" style="width:100%" />
</el-form-item>
<el-form-item label="计划结束时间" prop="plannedEndTime">
<el-date-picker clearable v-model="form.plannedEndTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="请选择" style="width:100%" />
<el-form-item label="计划说明" prop="planDescription">
<el-input v-model="form.planDescription" type="textarea" :rows="3" placeholder="请输入计划说明" />
</el-form-item>
<el-form-item label="负责部门" prop="dutyDept">
<el-input v-model="form.dutyDept" placeholder="请输入负责部门" />
</el-form-item>
<el-form-item label="计划负责人" prop="planOwner">
<el-form-item label="负责人" prop="planOwner">
<el-input v-model="form.planOwner" placeholder="请输入负责人" />
</el-form-item>
<el-form-item label="预算金额(元)" prop="budgetAmount">
<el-input-number v-model="form.budgetAmount" :min="0" :precision="2" style="width:100%" placeholder="请输入预算金额" />
</el-form-item>
<el-form-item label="计划说明" prop="planDescription">
<el-input v-model="form.planDescription" type="textarea" :rows="3" placeholder="请输入计划说明" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
@@ -214,39 +196,6 @@
</div>
</el-dialog>
<!-- 选择异常巡检记录弹窗 -->
<el-dialog title="选择异常巡检记录" :visible.sync="abnormalSelectorVisible" width="800px" append-to-body>
<el-form :model="abnormalQuery" size="small" :inline="true" style="margin-bottom:12px;">
<el-form-item label="产线">
<el-select v-model="abnormalQuery.productionLine" placeholder="请选择" clearable @change="loadAbnormalRecords" style="width:140px;">
<el-option v-for="item in lineList" :key="item.lineId" :label="item.lineName" :value="item.lineId" />
</el-select>
</el-form-item>
<el-form-item label="巡检人">
<el-input v-model="abnormalQuery.inspector" placeholder="搜索" clearable style="width:120px;" @keyup.enter.native="loadAbnormalRecords" />
</el-form-item>
<el-form-item>
<el-button type="primary" size="mini" @click="loadAbnormalRecords">搜索</el-button>
</el-form-item>
</el-form>
<el-table ref="abnormalTable" :data="abnormalRecordList" border size="small" @selection-change="handleAbnormalSelectionChange" max-height="400">
<el-table-column type="selection" width="45" />
<el-table-column label="设备部件" align="center" prop="partName" width="130" />
<el-table-column label="产线" align="center" prop="productionLine" width="110" />
<el-table-column label="班次" align="center" width="70">
<template slot-scope="scope">{{ scope.row.shift == 1 ? '白班' : '夜班' }}</template>
</el-table-column>
<el-table-column label="巡检时间" align="center" prop="inspectTime" width="150">
<template slot-scope="scope">{{ parseTime(scope.row.inspectTime, '{y}-{m}-{d} {h}:{i}') }}</template>
</el-table-column>
<el-table-column label="异常描述" align="center" prop="abnormalDesc" min-width="160" show-overflow-tooltip />
</el-table>
<div slot="footer" class="dialog-footer">
<el-button @click="abnormalSelectorVisible = false"> </el-button>
<el-button type="primary" @click="confirmAbnormalSelection"> 已选 {{ abnormalSelectedRecords.length }} </el-button>
</div>
</el-dialog>
<!-- 明细编辑弹窗 -->
<el-dialog :title="detailDialogTitle" :visible.sync="detailDialogVisible" width="550px" append-to-body>
<el-form ref="detailForm" :model="detailForm" label-width="110px">
@@ -288,7 +237,6 @@
<script>
import { listMaintenancePlan, getMaintenancePlan, addMaintenancePlan, updateMaintenancePlan, delMaintenancePlan } from "@/api/flow/maintenancePlan";
import { listMaintenancePlanDetail, addMaintenancePlanDetail, updateMaintenancePlanDetail, delMaintenancePlanDetail } from "@/api/flow/maintenancePlanDetail";
import { listMaintenancePlanAbnormal, addMaintenancePlanAbnormal, delMaintenancePlanAbnormal } from "@/api/flow/maintenancePlanAbnormal";
import { listEquipmentInspectionRecord } from "@/api/mes/eqp/equipmentInspectionRecord";
import { listProductionLine } from "@/api/wms/productionLine";
import DragResizePanel from "@/components/DragResizePanel/index.vue";
@@ -302,8 +250,7 @@ export default {
loading: false,
detailLoading: false,
buttonLoading: false,
submitLoading: false,
abnormalLoading: false,
inspectionLoading: false,
detailButtonLoading: false,
total: 0,
dataList: [],
@@ -313,17 +260,19 @@ export default {
form: {},
rules: {
planName: [{ required: true, message: "请输入计划名称", trigger: "blur" }],
productionLine: [{ required: true, message: "请选择产线", trigger: "change" }],
plannedStartTime: [{ required: true, message: "请选择开始时间", trigger: "change" }],
plannedEndTime: [{ required: true, message: "请选择结束时间", trigger: "change" }],
repairType: [{ required: true, message: "请选择维修类型", trigger: "change" }]
},
queryParams: { pageNum: 1, pageSize: 10, planNo: undefined, approvalStatus: undefined },
// Abnormal records
abnormalList: [],
abnormalSelectorVisible: false,
abnormalQuery: { productionLine: undefined, inspector: undefined },
abnormalRecordList: [],
abnormalSelectedRecords: [],
queryParams: { pageNum: 1, pageSize: 10, planNo: undefined },
lineList: [],
// Details
// 检修记录
inspectionRecords: [],
inspectionTotal: 0,
inspectionQueried: false,
selectedInspectionRows: [],
// 维修明细
detailList: [],
detailDialogVisible: false,
detailDialogTitle: "",
@@ -358,6 +307,9 @@ export default {
},
handleRowClick(row) {
this.currentRow = row;
this.inspectionQueried = false;
this.inspectionRecords = [];
this.selectedInspectionRows = [];
this.loadDetail(row.planId);
},
loadDetail(planId) {
@@ -365,19 +317,9 @@ export default {
var self = this;
getMaintenancePlan(planId).then(function(response) {
self.currentRow = response.data;
self.loadAbnormalList(planId);
self.loadDetailList(planId);
}).finally(function() { self.detailLoading = false; });
},
loadAbnormalList(planId) {
this.abnormalLoading = true;
var self = this;
listMaintenancePlanAbnormal({ planId: planId, pageNum: 1, pageSize: 999 }).then(function(r) {
var rows = r.rows || [];
// Enrich with inspection record info — query each record
self.abnormalList = rows;
self.abnormalLoading = false;
}).catch(function() { self.abnormalLoading = false; });
self.detailLoading = false;
}).catch(function() { self.detailLoading = false; });
},
loadDetailList(planId) {
var self = this;
@@ -385,16 +327,73 @@ export default {
self.detailList = r.rows || [];
});
},
handleRefreshDetail() {
if (this.currentRow && this.currentRow.planId) {
this.loadDetail(this.currentRow.planId);
// ---- 检修记录查询 ----
queryInspectionRecords() {
if (!this.currentRow || !this.currentRow.productionLine) {
this.$modal.msgWarning("请先在编辑中设置产线");
return;
}
this.inspectionLoading = true;
var self = this;
var params = {
pageNum: 1,
pageSize: 9999,
productionLine: this.getProductionLineId(this.currentRow.productionLine),
startTime: this.currentRow.plannedStartTime ? parseTime(this.currentRow.plannedStartTime, '{y}-{m}-{d}') : undefined,
endTime: this.currentRow.plannedEndTime ? parseTime(this.currentRow.plannedEndTime, '{y}-{m}-{d}') : undefined
};
listEquipmentInspectionRecord(params).then(function(response) {
self.inspectionRecords = response.rows || [];
self.inspectionTotal = response.total || 0;
self.inspectionQueried = true;
self.$nextTick(function() {
if (self.$refs.inspectionTable) {
self.$refs.inspectionTable.clearSelection();
}
});
}).finally(function() {
self.inspectionLoading = false;
});
},
// ---- Plan CRUD ----
getProductionLineId(name) {
var line = this.lineList.find(function(l) { return l.lineName === name; });
return line ? line.lineId : null;
},
handleInspectionSelectionChange(selection) {
this.selectedInspectionRows = selection;
},
addDetailFromSelection() {
if (this.selectedInspectionRows.length === 0) return;
var self = this;
var planId = this.currentRow.planId;
this.$modal.loading("正在添加维修明细...");
var promises = this.selectedInspectionRows.map(function(rec) {
return addMaintenancePlanDetail({
planId: planId,
componentName: rec.partName || '',
productionLine: self.currentRow.productionLine || rec.productionLine || '',
maintenanceCategory: 1,
repairContent: rec.abnormalDesc || '',
repairUser: rec.inspector || '',
detailStatus: 0
});
});
Promise.all(promises).then(function() {
self.$modal.closeLoading();
self.$modal.msgSuccess("已从检修记录添加 " + self.selectedInspectionRows.length + " 条明细");
self.loadDetailList(planId);
self.$refs.inspectionTable.clearSelection();
self.selectedInspectionRows = [];
}).catch(function() {
self.$modal.closeLoading();
self.$modal.msgError("操作失败");
});
},
// ---- 计划 CRUD ----
handleAdd() {
this.reset();
this.open = true;
this.title = "新增维修计划";
this.title = "新增检修单";
},
handleUpdate(row) {
this.reset();
@@ -402,17 +401,18 @@ export default {
getMaintenancePlan(row.planId).then(function(response) {
self.form = response.data;
self.open = true;
self.title = "修改维修计划";
self.title = "修改检修单";
});
},
reset() {
this.form = {
planId: undefined, planNo: undefined, planName: undefined,
productionLine: undefined,
repairType: undefined, priorityLevel: 1,
plannedStartTime: undefined, plannedEndTime: undefined,
dutyDept: undefined, planOwner: undefined,
budgetAmount: undefined, planDescription: undefined,
approvalStatus: 0, remark: undefined
remark: undefined
};
this.resetForm("form");
},
@@ -447,7 +447,7 @@ export default {
},
handleDelete(row) {
var self = this;
this.$modal.confirm('是否确认删除维修计划"' + row.planNo + '"').then(function() {
this.$modal.confirm('是否确认删除检修单"' + row.planNo + '"').then(function() {
self.loading = true;
return delMaintenancePlan(row.planId);
}).then(function() {
@@ -455,104 +455,25 @@ export default {
self.getList();
if (self.currentRow && self.currentRow.planId === row.planId) {
self.currentRow = null;
self.abnormalList = [];
self.detailList = [];
self.inspectionRecords = [];
self.inspectionQueried = false;
}
self.$modal.msgSuccess("删除成功");
}).catch(function() { }).finally(function() { self.loading = false; });
},
// ---- Submit for approval ----
handleSubmitApproval() {
var self = this;
if (this.detailList.length === 0) {
this.$modal.msgWarning("请至少添加一条维修明细");
return;
}
this.$modal.confirm('确认提交维修计划"' + this.currentRow.planNo + '"进行审批?').then(function() {
self.submitLoading = true;
return updateMaintenancePlan({ planId: self.currentRow.planId, approvalStatus: 1 });
}).then(function() {
self.$modal.msgSuccess("提交审批成功");
self.submitLoading = false;
self.loadDetail(self.currentRow.planId);
self.getList();
}).catch(function() { self.submitLoading = false; });
},
// ---- Abnormal records selection ----
handleSelectAbnormalRecords() {
this.abnormalSelectedRecords = [];
this.abnormalQuery = { productionLine: undefined, inspector: undefined };
this.loadAbnormalRecords();
this.abnormalSelectorVisible = true;
},
loadAbnormalRecords() {
var self = this;
var params = {
pageNum: 1, pageSize: 999,
productionLine: this.abnormalQuery.productionLine,
inspector: this.abnormalQuery.inspector,
runStatus: 0 // 异常记录
};
listEquipmentInspectionRecord(params).then(function(response) {
self.abnormalRecordList = response.rows || [];
});
},
handleAbnormalSelectionChange(selection) {
this.abnormalSelectedRecords = selection;
},
confirmAbnormalSelection() {
if (this.abnormalSelectedRecords.length === 0) {
this.$modal.msgWarning("请至少选择一条异常记录");
return;
}
var self = this;
var planId = this.currentRow.planId;
var addPromises = this.abnormalSelectedRecords.map(function(rec) {
return addMaintenancePlanAbnormal({ planId: planId, recordId: rec.recordId });
});
// Also auto-create detail rows from selected records
this.$modal.loading("正在关联异常记录并生成维修明细...");
Promise.all(addPromises).then(function() {
// Create detail rows
var detailPromises = self.abnormalSelectedRecords.map(function(rec) {
return addMaintenancePlanDetail({
planId: planId,
recordId: rec.recordId,
componentName: rec.partName || '',
productionLine: rec.productionLine || '',
maintenanceCategory: 1,
detailStatus: 0
});
});
return Promise.all(detailPromises);
}).then(function() {
self.$modal.closeLoading();
self.$modal.msgSuccess("关联成功,已生成维修明细");
self.abnormalSelectorVisible = false;
self.loadAbnormalList(planId);
self.loadDetailList(planId);
}).catch(function() {
self.$modal.closeLoading();
self.$modal.msgError("操作失败");
});
},
handleRemoveAbnormal(row) {
var self = this;
this.$modal.confirm('确认移除该异常记录关联?').then(function() {
return delMaintenancePlanAbnormal(row.relId);
}).then(function() {
self.$modal.msgSuccess("移除成功");
self.loadAbnormalList(self.currentRow.planId);
}).catch(function() { });
},
// ---- Detail management ----
// ---- 明细管理 ----
handleAddDetail() {
this.detailForm = {
planId: this.currentRow.planId,
componentName: '', productionLine: '',
maintenanceCategory: 1, repairContent: '',
repairUser: '', itemPlanDate: undefined,
targetManufacturer: '', detailStatus: 0
componentName: '',
productionLine: this.currentRow.productionLine || '',
maintenanceCategory: 1,
repairContent: '',
repairUser: '',
itemPlanDate: undefined,
targetManufacturer: '',
detailStatus: 0
};
this.editingDetailId = null;
this.detailDialogTitle = "添加维修明细";
@@ -606,8 +527,8 @@ export default {
.panel-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px 8px; background: #f5f7fa; }
.header-title { display: flex; align-items: center; gap: 6px; font-size: 14px; font-weight: 600; color: #303133; }
.header-title i { color: #409eff; font-size: 16px; }
.header-filter { width: 120px; }
.search-row { display: flex; align-items: center; gap: 6px; padding: 0 14px 10px; background: #f5f7fa; }
.search-row .el-input { flex: 1; }
.list-body { flex: 1; overflow-y: auto; padding: 0 6px; }
.list-item { display: flex; align-items: center; padding: 10px 12px; margin-bottom: 2px; cursor: pointer; border-radius: 6px; transition: all 0.15s; }
.list-item:hover { background: #ebeef5; }
@@ -616,51 +537,42 @@ export default {
.item-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.item-title { font-size: 13px; font-weight: 500; color: #303133; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.item-sub { font-size: 12px; color: #909399; }
.item-meta { flex-shrink: 0; margin: 0 8px; }
.item-actions { flex-shrink: 0; opacity: 0; transition: opacity 0.15s; }
.list-item:hover .item-actions { opacity: 1; }
.list-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 0; color: #c0c4cc; font-size: 13px; gap: 8px; }
.list-empty i { font-size: 32px; }
.list-footer { border-top: 1px solid #e4e7ed; padding: 2px 8px 0; background: #f5f7fa; }
/* ========== 右侧面板 — Word 文档风格 ========== */
.right-panel { height: 100%; overflow-y: auto; padding: 12px 16px; background: #faf8f5; }
.right-panel .detail-content { margin: 0 auto; background: #ffffff; padding: 28px 32px 36px; box-shadow: 0 1px 4px rgba(0,0,0,0.06), 0 2px 12px rgba(0,0,0,0.04); min-height: 100%; }
/* ========== 右侧面板 ========== */
.right-panel { height: 100%; overflow: hidden; display: flex; flex-direction: column; background: #faf8f5; }
.empty-tip { display: flex; align-items: center; justify-content: center; height: 100%; color: #909399; font-size: 14px; gap: 8px; }
.right-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; padding: 12px 16px; }
.doc-header { margin-bottom: 18px; padding-bottom: 14px; border-bottom: 2px solid #1a3c6e; }
.doc-header-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.doc-title-group { flex: 1; min-width: 0; }
.doc-title { font-family: 'Georgia', 'Times New Roman', 'Noto Serif SC', 'SimSun', serif; font-size: 24px; font-weight: 700; color: #1a1a1a; line-height: 1.3; letter-spacing: 0.5px; }
.doc-subtitle { font-family: 'Georgia', 'Times New Roman', serif; font-size: 12px; font-weight: 400; color: #8c8c8c; font-style: italic; letter-spacing: 0.8px; margin-top: 2px; }
.doc-header-right { flex-shrink: 0; }
.doc-status-row { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
.doc-status-label { font-family: 'Georgia', 'Times New Roman', serif; font-size: 11px; color: #8c8c8c; letter-spacing: 0.3px; }
/* 基本信息栏 */
.info-bar { display: flex; align-items: center; justify-content: space-between; padding: 8px 16px; background: #fff; border: 1px solid #e4e7ed; border-radius: 4px; margin-bottom: 10px; flex-shrink: 0; }
.info-bar-left { display: flex; align-items: center; gap: 6px; font-size: 13px; }
.info-label { color: #909399; }
.info-value { color: #303133; font-weight: 500; }
.info-bar-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.detail-meta { display: flex; flex-wrap: wrap; gap: 16px; font-size: 12px; color: #909399; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #e0dcd6; }
.detail-meta span { display: inline-flex; align-items: center; gap: 4px; }
.detail-meta i { font-size: 13px; }
/* 上半部分:检修记录 */
.inspection-section { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; background: #fff; border: 1px solid #e4e7ed; border-radius: 4px; margin-bottom: 10px; }
.section-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-bottom: 1px solid #ebeef5; flex-shrink: 0; }
.section-title { font-size: 13px; font-weight: 600; color: #303133; }
.section-title .en-sub { font-size: 11px; font-weight: 400; color: #c0c4cc; font-style: italic; }
.section-count { font-size: 12px; color: #909399; }
.section-actions { display: flex; align-items: center; gap: 6px; }
.inspection-section .el-table { flex: 1; overflow-y: auto; }
.section-title { font-family: 'Georgia', 'Times New Roman', 'Noto Serif SC', 'SimSun', serif; width: 100%; font-size: 15px; font-weight: 700; color: #1a1a1a; margin: 22px 0 12px 0; padding: 0 0 10px 0; border-bottom: 1px solid #d4d0c8; display: flex; align-items: center; gap: 10px; letter-spacing: 0.3px; }
.section-title:first-child { margin-top: 0; }
.section-title .en-sub { font-size: 11px; font-weight: 400; color: #8c8c8c; letter-spacing: 0.5px; font-family: 'Georgia', 'Times New Roman', serif; font-style: italic; }
.section-title i { font-size: 16px; color: #1a3c6e; }
/* 下半部分:维修明细 */
.detail-section { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; background: #fff; border: 1px solid #e4e7ed; border-radius: 4px; }
.detail-section .el-table { flex: 1; overflow-y: auto; }
.remark-content { padding: 12px 16px; background: #faf8f5; border: 1px solid #e8e4de; border-radius: 2px; font-size: 13px; line-height: 1.8; color: #1a1a1a; }
.section-gap { height: 16px; }
.empty-data { color: #8c8c8c; font-size: 13px; font-style: italic; }
.empty-data { padding: 24px 0; text-align: center; color: #c0c4cc; font-size: 13px; }
.form-actions { display: flex; justify-content: flex-end; gap: 10px; padding: 16px 0; border-top: 1px solid #e0dcd6; margin-top: 8px; }
.right-panel .el-table { border: 1px solid #e8e4de !important; border-radius: 2px !important; font-size: 12px !important; }
.right-panel .el-table thead th { background-color: #2c3e50 !important; color: #ffffff !important; font-weight: 600 !important; font-size: 11px !important; letter-spacing: 0.5px !important; border-bottom: none !important; font-family: 'Georgia', 'Times New Roman', serif; }
.right-panel .el-table thead th .cell { color: #ffffff !important; }
.right-panel .el-table__body tr:hover > td { background-color: #f7f5f0 !important; }
.right-panel .el-table--border td { border-right: 1px solid #f0ece6 !important; }
.right-panel .el-table--border th { border-right: 1px solid #3a5166 !important; }
.right-panel .el-table td { padding: 6px 4px !important; color: #3a3a3a !important; }
.right-panel .el-divider--horizontal { margin: 8px 0 4px; background-color: #e0dcd6; }
.right-panel .el-tag { border-radius: 2px; font-family: 'Georgia', 'Times New Roman', serif; letter-spacing: 0.3px; }
.right-panel .el-tag--mini { padding: 0 6px; line-height: 20px; height: 20px; }
.right-panel .el-tag--small { padding: 0 8px; }
/* 表格样式 */
.right-panel .el-table { border: none !important; }
.right-panel .el-table th { background: #f5f7fa !important; color: #606266 !important; font-size: 12px !important; padding: 6px 0 !important; }
.right-panel .el-table td { padding: 6px 0 !important; font-size: 12px !important; color: #303133 !important; }
.right-panel .el-divider--vertical { margin: 0 6px; }
</style>

View File

@@ -110,59 +110,57 @@ export default {
<style scoped>
.detail-section {
margin-bottom: 20px;
margin-bottom: 14px;
}
.detail-section-label {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 11px;
font-weight: 600;
color: #8c8c8c;
color: #64748b;
margin-bottom: 4px;
letter-spacing: 0.8px;
letter-spacing: 0.4px;
text-transform: uppercase;
}
.detail-section-text {
font-size: 14px;
color: #1a1a1a;
line-height: 1.8;
font-size: 13px;
color: #1e293b;
line-height: 1.6;
word-break: break-all;
padding-bottom: 12px;
border-bottom: 1px solid #eeeae4;
padding-bottom: 10px;
border-bottom: 1px solid #e2e8f0;
}
/* ===== 客户信息卡片 ===== */
.customer-info-card {
padding: 10px 14px;
background: #faf8f5;
border: 1px solid #e8e4de;
border-radius: 2px;
margin-bottom: 6px;
padding: 8px 12px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 0;
margin-bottom: 4px;
}
.customer-row {
display: flex;
gap: 14px;
padding: 3px 0;
gap: 12px;
padding: 2px 0;
}
.customer-row + .customer-row {
border-top: 1px solid #f0ece6;
border-top: 1px solid #e2e8f0;
}
.customer-row-label {
flex-shrink: 0;
width: 70px;
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 11px;
color: #8c8c8c;
letter-spacing: 0.3px;
color: #64748b;
letter-spacing: 0.2px;
}
.customer-row-value {
flex: 1;
font-size: 13px;
color: #1a1a1a;
color: #1e293b;
}
</style>

View File

@@ -60,19 +60,18 @@ export default {
<style scoped>
.section-title {
font-family: 'Georgia', 'Times New Roman', 'Noto Serif SC', 'SimSun', serif;
width: 100%;
font-size: 15px;
font-weight: 700;
color: #1a1a1a;
margin: 32px 0 16px 0;
padding: 0 0 10px 0;
border-bottom: 1px solid #d4d0c8;
font-size: 13px;
font-weight: 600;
color: #1e293b;
margin: 20px 0 10px 0;
padding: 0 0 8px 0;
border-bottom: 1px solid #cbd5e1;
white-space: nowrap;
display: flex;
align-items: center;
gap: 10px;
letter-spacing: 0.3px;
gap: 8px;
letter-spacing: 0.2px;
}
.section-title:first-child {
margin-top: 0;
@@ -80,19 +79,16 @@ export default {
.section-title .en-sub {
font-size: 11px;
font-weight: 400;
color: #8c8c8c;
letter-spacing: 0.5px;
font-family: 'Georgia', 'Times New Roman', serif;
font-style: italic;
color: #94a3b8;
letter-spacing: 0.2px;
}
.section-title i {
font-size: 16px;
color: #1a3c6e;
font-size: 14px;
color: #475569;
}
.empty-data {
color: #8c8c8c;
font-size: 13px;
padding: 8px 0;
font-style: italic;
color: #94a3b8;
font-size: 12px;
padding: 6px 0;
}
</style>

View File

@@ -51,19 +51,18 @@ export default {
<style scoped>
.section-title {
font-family: 'Georgia', 'Times New Roman', 'Noto Serif SC', 'SimSun', serif;
width: 100%;
font-size: 15px;
font-weight: 700;
color: #1a1a1a;
margin: 32px 0 16px 0;
padding: 0 0 10px 0;
border-bottom: 1px solid #d4d0c8;
font-size: 13px;
font-weight: 600;
color: #1e293b;
margin: 20px 0 10px 0;
padding: 0 0 8px 0;
border-bottom: 1px solid #cbd5e1;
white-space: nowrap;
display: flex;
align-items: center;
gap: 10px;
letter-spacing: 0.3px;
gap: 8px;
letter-spacing: 0.2px;
}
.section-title:first-child {
margin-top: 0;
@@ -71,19 +70,16 @@ export default {
.section-title .en-sub {
font-size: 11px;
font-weight: 400;
color: #8c8c8c;
letter-spacing: 0.5px;
font-family: 'Georgia', 'Times New Roman', serif;
font-style: italic;
color: #94a3b8;
letter-spacing: 0.2px;
}
.section-title i {
font-size: 16px;
color: #1a3c6e;
font-size: 14px;
color: #475569;
}
.empty-data {
color: #8c8c8c;
font-size: 13px;
padding: 8px 0;
font-style: italic;
color: #94a3b8;
font-size: 12px;
padding: 6px 0;
}
</style>

View File

@@ -93,19 +93,18 @@ export default {
<style scoped>
.section-title {
font-family: 'Georgia', 'Times New Roman', 'Noto Serif SC', 'SimSun', serif;
width: 100%;
font-size: 15px;
font-weight: 700;
color: #1a1a1a;
margin: 32px 0 16px 0;
padding: 0 0 10px 0;
border-bottom: 1px solid #d4d0c8;
font-size: 13px;
font-weight: 600;
color: #1e293b;
margin: 20px 0 10px 0;
padding: 0 0 8px 0;
border-bottom: 1px solid #cbd5e1;
white-space: nowrap;
display: flex;
align-items: center;
gap: 10px;
letter-spacing: 0.3px;
gap: 8px;
letter-spacing: 0.2px;
}
.section-title:first-child {
margin-top: 0;
@@ -113,29 +112,27 @@ export default {
.section-title .en-sub {
font-size: 11px;
font-weight: 400;
color: #8c8c8c;
letter-spacing: 0.5px;
font-family: 'Georgia', 'Times New Roman', serif;
font-style: italic;
color: #94a3b8;
letter-spacing: 0.2px;
}
.section-title i {
font-size: 16px;
color: #1a3c6e;
font-size: 14px;
color: #475569;
}
/* ===== 意见卡片(正式文档风格 ===== */
/* ===== 意见卡片 — 工业风格 ===== */
.card-grid {
display: flex;
flex-wrap: wrap;
gap: 14px;
gap: 10px;
}
.opinion-card {
flex: 0 0 calc((100% - 14px) / 2);
flex: 0 0 calc((100% - 10px) / 2);
background: #ffffff;
border: 1px solid #e8e4de;
border-radius: 2px;
padding: 14px 16px 12px;
border: 1px solid #e2e8f0;
border-radius: 0;
padding: 10px 14px 10px;
display: flex;
flex-direction: column;
}
@@ -144,25 +141,24 @@ export default {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px dashed #e0dcd6;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid #e2e8f0;
}
.opinion-dept {
display: flex;
align-items: center;
gap: 6px;
font-family: 'Georgia', 'Times New Roman', 'Noto Serif SC', 'SimSun', serif;
font-size: 14px;
font-weight: 700;
color: #1a3c6e;
letter-spacing: 0.3px;
font-size: 13px;
font-weight: 600;
color: #1e293b;
letter-spacing: 0.2px;
}
.opinion-dept-icon {
font-size: 10px;
color: #1a3c6e;
font-size: 8px;
color: #475569;
}
.opinion-card-body {
@@ -170,11 +166,11 @@ export default {
}
.opinion-content {
font-size: 13px;
color: #3a3a3a;
line-height: 1.7;
font-size: 12px;
color: #475569;
line-height: 1.6;
word-break: break-all;
max-height: 100px;
max-height: 90px;
overflow-y: auto;
}
@@ -183,35 +179,31 @@ export default {
}
.opinion-empty {
color: #bab5ae;
color: #94a3b8;
font-size: 12px;
font-style: italic;
font-family: 'Georgia', 'Times New Roman', serif;
}
.opinion-file {
margin-top: 8px;
margin-top: 6px;
}
.opinion-card-footer {
display: flex;
flex-wrap: wrap;
gap: 14px;
margin-top: 10px;
padding-top: 8px;
border-top: 1px dashed #e0dcd6;
gap: 12px;
margin-top: 8px;
padding-top: 6px;
border-top: 1px solid #e2e8f0;
}
.opinion-footer-item {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 11px;
color: #8c8c8c;
color: #94a3b8;
}
.empty-data {
color: #8c8c8c;
font-size: 13px;
padding: 8px 0;
font-style: italic;
color: #94a3b8;
font-size: 12px;
padding: 6px 0;
}
</style>

View File

@@ -97,16 +97,16 @@ export default {
<style scoped>
.dept-preview-container {
font-size: 13px;
color: #3a3a3a;
line-height: 1.7;
font-size: 12px;
color: #475569;
line-height: 1.6;
}
.preview-row {
display: flex;
gap: 12px;
padding: 4px 0;
border-bottom: 1px solid #f0ece6;
gap: 10px;
padding: 3px 0;
border-bottom: 1px solid #e2e8f0;
}
.preview-row:last-child {
@@ -116,22 +116,19 @@ export default {
.preview-label {
flex-shrink: 0;
width: 100px;
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 11px;
color: #8c8c8c;
letter-spacing: 0.3px;
color: #64748b;
letter-spacing: 0.2px;
}
.preview-value {
flex: 1;
color: #1a1a1a;
color: #1e293b;
word-break: break-all;
}
.preview-empty {
color: #bab5ae;
color: #94a3b8;
font-size: 12px;
font-style: italic;
font-family: 'Georgia', 'Times New Roman', serif;
}
</style>

View File

@@ -78,19 +78,18 @@ export default {
<style scoped>
.section-title {
font-family: 'Georgia', 'Times New Roman', 'Noto Serif SC', 'SimSun', serif;
width: 100%;
font-size: 15px;
font-weight: 700;
color: #1a1a1a;
margin: 32px 0 16px 0;
padding: 0 0 10px 0;
border-bottom: 1px solid #d4d0c8;
font-size: 13px;
font-weight: 600;
color: #1e293b;
margin: 20px 0 10px 0;
padding: 0 0 8px 0;
border-bottom: 1px solid #cbd5e1;
white-space: nowrap;
display: flex;
align-items: center;
gap: 10px;
letter-spacing: 0.3px;
gap: 8px;
letter-spacing: 0.2px;
}
.section-title:first-child {
margin-top: 0;
@@ -98,29 +97,27 @@ export default {
.section-title .en-sub {
font-size: 11px;
font-weight: 400;
color: #8c8c8c;
letter-spacing: 0.5px;
font-family: 'Georgia', 'Times New Roman', serif;
font-style: italic;
color: #94a3b8;
letter-spacing: 0.2px;
}
.section-title i {
font-size: 16px;
color: #1a3c6e;
font-size: 14px;
color: #475569;
}
/* ===== 反馈卡片(正式文档风格 ===== */
/* ===== 反馈卡片 — 工业风格 ===== */
.card-grid {
display: flex;
flex-wrap: wrap;
gap: 14px;
gap: 10px;
}
.opinion-card {
flex: 0 0 calc((100% - 14px) / 2);
flex: 0 0 calc((100% - 10px) / 2);
background: #ffffff;
border: 1px solid #e8e4de;
border-radius: 2px;
padding: 14px 16px 12px;
border: 1px solid #e2e8f0;
border-radius: 0;
padding: 10px 14px 10px;
display: flex;
flex-direction: column;
}
@@ -129,25 +126,24 @@ export default {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px dashed #e0dcd6;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid #e2e8f0;
}
.opinion-dept {
display: flex;
align-items: center;
gap: 6px;
font-family: 'Georgia', 'Times New Roman', 'Noto Serif SC', 'SimSun', serif;
font-size: 14px;
font-weight: 700;
color: #1a3c6e;
letter-spacing: 0.3px;
font-size: 13px;
font-weight: 600;
color: #1e293b;
letter-spacing: 0.2px;
}
.opinion-dept-icon {
font-size: 10px;
color: #1a3c6e;
font-size: 8px;
color: #475569;
}
.opinion-card-body {
@@ -155,11 +151,11 @@ export default {
}
.opinion-content {
font-size: 13px;
color: #3a3a3a;
line-height: 1.7;
font-size: 12px;
color: #475569;
line-height: 1.6;
word-break: break-all;
max-height: 100px;
max-height: 90px;
overflow-y: auto;
}
@@ -168,35 +164,31 @@ export default {
}
.opinion-empty {
color: #bab5ae;
color: #94a3b8;
font-size: 12px;
font-style: italic;
font-family: 'Georgia', 'Times New Roman', serif;
}
.opinion-file {
margin-top: 8px;
margin-top: 6px;
}
.opinion-card-footer {
display: flex;
flex-wrap: wrap;
gap: 14px;
margin-top: 10px;
padding-top: 8px;
border-top: 1px dashed #e0dcd6;
gap: 12px;
margin-top: 8px;
padding-top: 6px;
border-top: 1px solid #e2e8f0;
}
.opinion-footer-item {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 11px;
color: #8c8c8c;
color: #94a3b8;
}
.empty-data {
color: #8c8c8c;
font-size: 13px;
padding: 8px 0;
font-style: italic;
color: #94a3b8;
font-size: 12px;
padding: 6px 0;
}
</style>

View File

@@ -65,55 +65,52 @@ export default {
<style scoped>
.section-container {
margin-bottom: 6px;
margin-bottom: 4px;
}
.section-title {
font-family: 'Georgia', 'Times New Roman', 'Noto Serif SC', 'SimSun', serif;
width: 100%;
font-size: 15px;
font-weight: 700;
color: #1a1a1a;
margin: 32px 0 16px 0;
padding: 0 0 10px 0;
border-bottom: 1px solid #d4d0c8;
font-size: 13px;
font-weight: 600;
color: #1e293b;
margin: 20px 0 10px 0;
padding: 0 0 8px 0;
border-bottom: 1px solid #cbd5e1;
display: flex;
align-items: center;
gap: 10px;
letter-spacing: 0.3px;
gap: 8px;
letter-spacing: 0.2px;
}
.section-title .en-sub {
font-size: 11px;
font-weight: 400;
color: #8c8c8c;
letter-spacing: 0.5px;
font-family: 'Georgia', 'Times New Roman', serif;
font-style: italic;
color: #94a3b8;
letter-spacing: 0.2px;
}
.section-title i {
font-size: 16px;
color: #1a3c6e;
font-size: 14px;
color: #475569;
}
.flow-steps {
padding: 8px 0 4px;
padding: 6px 0 4px;
}
.flow-steps >>> .el-step.is-wait .el-step__icon-inner,
.flow-steps >>> .el-step.is-wait .el-step__title {
color: #c0c4cc;
color: #94a3b8;
}
.flow-steps >>> .el-step.is-process .el-step__icon-inner,
.flow-steps >>> .el-step.is-process .el-step__title {
color: #409eff;
color: #2563eb;
}
.flow-steps >>> .el-step.is-finish .el-step__icon-inner,
.flow-steps >>> .el-step.is-finish .el-step__title {
color: #67c23a;
color: #52c41a;
}
.flow-steps >>> .el-step__description {
@@ -125,15 +122,14 @@ export default {
align-items: center;
justify-content: flex-end;
gap: 6px;
margin-top: 6px;
padding-top: 8px;
border-top: 1px dashed #e0dcd6;
margin-top: 4px;
padding-top: 6px;
border-top: 1px solid #e2e8f0;
}
.status-label {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 11px;
color: #8c8c8c;
letter-spacing: 0.3px;
color: #64748b;
letter-spacing: 0.2px;
}
</style>

View File

@@ -71,19 +71,18 @@ export default {
<style scoped>
.section-title {
font-family: 'Georgia', 'Times New Roman', 'Noto Serif SC', 'SimSun', serif;
width: 100%;
font-size: 15px;
font-weight: 700;
color: #1a1a1a;
margin: 32px 0 16px 0;
padding: 0 0 10px 0;
border-bottom: 1px solid #d4d0c8;
font-size: 13px;
font-weight: 600;
color: #1e293b;
margin: 20px 0 10px 0;
padding: 0 0 8px 0;
border-bottom: 1px solid #cbd5e1;
white-space: nowrap;
display: flex;
align-items: center;
gap: 10px;
letter-spacing: 0.3px;
gap: 8px;
letter-spacing: 0.2px;
}
.section-title:first-child {
margin-top: 0;
@@ -91,28 +90,25 @@ export default {
.section-title .en-sub {
font-size: 11px;
font-weight: 400;
color: #8c8c8c;
letter-spacing: 0.5px;
font-family: 'Georgia', 'Times New Roman', serif;
font-style: italic;
color: #94a3b8;
letter-spacing: 0.2px;
}
.section-title i {
font-size: 16px;
color: #1a3c6e;
font-size: 14px;
color: #475569;
}
.empty-data {
color: #8c8c8c;
font-size: 13px;
padding: 8px 0;
font-style: italic;
color: #94a3b8;
font-size: 12px;
padding: 6px 0;
}
.plan-content {
padding: 12px 16px;
background: #faf8f5;
border: 1px solid #e8e4de;
border-radius: 2px;
padding: 10px 14px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 0;
font-size: 13px;
line-height: 1.8;
color: #1a1a1a;
line-height: 1.6;
color: #1e293b;
}
</style>

View File

@@ -106,21 +106,21 @@ export default {
<style scoped>
.section-container {
margin-bottom: 4px;
margin-bottom: 2px;
}
/* ===== 文档标题 ===== */
.doc-header {
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 2px solid #1a3c6e;
margin-bottom: 18px;
padding-bottom: 14px;
border-bottom: 2px solid #1e293b;
}
.doc-header-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
gap: 12px;
}
.doc-title-group {
@@ -129,21 +129,18 @@ export default {
}
.doc-title {
font-family: 'Georgia', 'Times New Roman', 'Noto Serif SC', 'SimSun', serif;
font-size: 24px;
font-size: 20px;
font-weight: 700;
color: #1a1a1a;
color: #1e293b;
line-height: 1.3;
letter-spacing: 0.5px;
letter-spacing: 0.2px;
}
.doc-subtitle {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 12px;
font-size: 11px;
font-weight: 400;
color: #8c8c8c;
font-style: italic;
letter-spacing: 0.8px;
color: #64748b;
letter-spacing: 0.4px;
margin-top: 2px;
}
@@ -154,36 +151,35 @@ export default {
.doc-status-row {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
gap: 6px;
margin-top: 8px;
}
.doc-status-label {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 11px;
color: #8c8c8c;
letter-spacing: 0.3px;
color: #64748b;
letter-spacing: 0.2px;
}
/* ===== 元信息行(原始样式) ===== */
/* ===== 元信息行 ===== */
.detail-meta {
display: flex;
flex-wrap: wrap;
gap: 16px;
gap: 14px;
font-size: 12px;
color: #909399;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e0dcd6;
color: #64748b;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid #e2e8f0;
}
.detail-meta span {
display: inline-flex;
align-items: center;
gap: 4px;
gap: 3px;
}
.detail-meta i {
font-size: 13px;
font-size: 12px;
}
</style>

View File

@@ -563,6 +563,7 @@ export default {
height: calc(100vh - 84px);
}
/* ========== 左侧面板 — 工业风格 ========== */
.left-panel {
display: flex;
flex-direction: column;
@@ -639,7 +640,7 @@ export default {
min-width: 0;
display: flex;
flex-direction: column;
gap: 3px;
gap: 2px;
}
.item-title {
@@ -676,35 +677,34 @@ export default {
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 0;
color: #c0c4cc;
padding: 48px 0;
color: #94a3b8;
font-size: 13px;
gap: 8px;
}
.list-empty i {
font-size: 32px;
font-size: 28px;
}
.list-footer {
border-top: 1px solid #e4e7ed;
padding: 2px 8px 0;
background: #f5f7fa;
border-top: 1px solid #cbd5e1;
padding: 0 4px;
background: #e8ecf1;
}
/* ========== 右侧面板 — Word 文档风格 ========== */
/* ========== 右侧面板 ========== */
.right-panel {
height: 100%;
overflow-y: auto;
padding: 12px 16px;
background: #faf8f5; /* 暖白纸张底色 */
padding: 0;
background: #f1f5f9;
}
.right-panel .detail-content {
margin: 0 auto;
background: #ffffff;
padding: 28px 32px 36px;
box-shadow: 0 1px 4px rgba(0,0,0,0.06), 0 2px 12px rgba(0,0,0,0.04);
padding: 20px 24px 24px;
min-height: 100%;
}
@@ -713,48 +713,46 @@ export default {
align-items: center;
justify-content: center;
height: 100%;
color: #909399;
font-size: 14px;
gap: 8px;
color: #94a3b8;
font-size: 13px;
gap: 6px;
}
.detail-section {
margin-bottom: 20px;
margin-bottom: 14px;
}
.detail-section-label {
font-size: 11px;
font-weight: 600;
color: #8c8c8c;
color: #64748b;
margin-bottom: 4px;
letter-spacing: 0.8px;
letter-spacing: 0.4px;
text-transform: uppercase;
font-family: 'Georgia', 'Times New Roman', serif;
}
.detail-section-text {
font-size: 14px;
color: #1a1a1a;
line-height: 1.8;
font-size: 13px;
color: #1e293b;
line-height: 1.6;
word-break: break-all;
padding-bottom: 12px;
border-bottom: 1px solid #eeeae4;
padding-bottom: 10px;
border-bottom: 1px solid #e2e8f0;
}
/* 文档级通用 section 标题 */
/* 通用 section 标题 */
.section-title {
font-family: 'Georgia', 'Times New Roman', 'Noto Serif SC', 'SimSun', serif;
width: 100%;
font-size: 15px;
font-weight: 700;
color: #1a1a1a;
margin: 22px 0 12px 0;
padding: 0 0 10px 0;
border-bottom: 1px solid #d4d0c8;
font-size: 13px;
font-weight: 600;
color: #1e293b;
margin: 18px 0 10px 0;
padding: 0 0 8px 0;
border-bottom: 1px solid #cbd5e1;
display: flex;
align-items: center;
gap: 10px;
letter-spacing: 0.3px;
gap: 8px;
letter-spacing: 0.2px;
}
.section-title:first-child {
@@ -764,96 +762,91 @@ export default {
.section-title .en-sub {
font-size: 11px;
font-weight: 400;
color: #8c8c8c;
letter-spacing: 0.5px;
font-family: 'Georgia', 'Times New Roman', serif;
font-style: italic;
color: #94a3b8;
letter-spacing: 0.2px;
}
.section-title i {
font-size: 16px;
color: #1a3c6e;
font-size: 14px;
color: #475569;
}
.empty-data {
color: #8c8c8c;
font-size: 13px;
padding: 8px 0;
font-style: italic;
color: #94a3b8;
font-size: 12px;
padding: 6px 0;
}
.plan-content {
padding: 12px 16px;
background: #faf8f5;
border: 1px solid #e8e4de;
border-radius: 2px;
padding: 10px 14px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 0;
font-size: 13px;
line-height: 1.8;
color: #1a1a1a;
line-height: 1.6;
color: #1e293b;
}
.section-gap {
height: 16px;
height: 12px;
}
/* 正式表格覆写 */
/* 表格覆写 */
.right-panel .el-table {
border: 1px solid #e8e4de !important;
border-radius: 2px !important;
border: 1px solid #cbd5e1 !important;
border-radius: 0 !important;
font-size: 12px !important;
}
.right-panel .el-table thead th {
background-color: #2c3e50 !important;
color: #ffffff !important;
font-weight: 600 !important;
background-color: #334155 !important;
color: #f1f5f9 !important;
font-weight: 500 !important;
font-size: 11px !important;
letter-spacing: 0.5px !important;
letter-spacing: 0.3px !important;
border-bottom: none !important;
font-family: 'Georgia', 'Times New Roman', serif;
}
.right-panel .el-table thead th .cell {
color: #ffffff !important;
color: #f1f5f9 !important;
}
.right-panel .el-table__body tr:hover > td {
background-color: #f7f5f0 !important;
background-color: #f1f5f9 !important;
}
.right-panel .el-table--border td {
border-right: 1px solid #f0ece6 !important;
border-right: 1px solid #e2e8f0 !important;
}
.right-panel .el-table--border th {
border-right: 1px solid #3a5166 !important;
border-right: 1px solid #475569 !important;
}
.right-panel .el-table td {
padding: 6px 4px !important;
color: #3a3a3a !important;
padding: 5px 4px !important;
color: #1e293b !important;
}
.right-panel .el-divider--horizontal {
margin: 8px 0 4px;
background-color: #e0dcd6;
margin: 6px 0 2px;
background-color: #e2e8f0;
}
/* el-tag 文档风格微调 */
/* el-tag 工业风格 */
.right-panel .el-tag {
border-radius: 2px;
font-family: 'Georgia', 'Times New Roman', serif;
letter-spacing: 0.3px;
border-radius: 0;
letter-spacing: 0.2px;
}
.right-panel .el-tag--mini {
padding: 0 6px;
line-height: 20px;
height: 20px;
padding: 0 5px;
line-height: 18px;
height: 18px;
}
.right-panel .el-tag--small {
padding: 0 8px;
padding: 0 7px;
}
/* ===== PDF 导出时隐藏操作按钮 ===== */

View File

@@ -42,6 +42,17 @@ public class AttendanceRecordsController extends BaseController {
ExcelUtil.exportExcel(list, "打卡记录", AttendanceRecordsVo.class, response);
}
@Log(title = "考勤记录表导出", businessType = BusinessType.EXPORT)
@PostMapping("/exportReport")
public void exportReport(@RequestParam String startTime,
@RequestParam String endTime,
@RequestParam(required = false) String pin,
@RequestParam(required = false) String ename,
@RequestParam(required = false) String deptname,
HttpServletResponse response) {
iAttendanceRecordsService.exportReport(startTime, endTime, pin, ename, deptname, response);
}
@GetMapping("/{id}")
public R<AttendanceRecordsVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Integer id) {

View File

@@ -18,7 +18,9 @@ import com.klp.common.core.validate.EditGroup;
import com.klp.common.enums.BusinessType;
import com.klp.common.utils.poi.ExcelUtil;
import com.klp.domain.vo.WmsCoilQualityRejudgeVo;
import com.klp.domain.vo.WmsMaterialCoilRejudgeVo;
import com.klp.domain.bo.WmsCoilQualityRejudgeBo;
import com.klp.domain.bo.WmsMaterialCoilBo;
import com.klp.service.IWmsCoilQualityRejudgeService;
import com.klp.common.core.page.TableDataInfo;
@@ -44,6 +46,15 @@ public class WmsCoilQualityRejudgeController extends BaseController {
return iWmsCoilQualityRejudgeService.queryPageList(bo, pageQuery);
}
/**
* 查询钢卷信息并附带质量改判记录
* 调用钢卷物料表 queryList 查询钢卷列表,同时批量查询每个钢卷的最新一条质量改判记录
*/
@GetMapping("/coilListWithRejudge")
public R<List<WmsMaterialCoilRejudgeVo>> coilListWithRejudge(WmsMaterialCoilBo bo) {
return R.ok(iWmsCoilQualityRejudgeService.queryCoilListWithRejudge(bo));
}
/**
* 导出钢卷质量改判记录列表
*/

View File

@@ -6,6 +6,7 @@ import com.klp.domain.bo.AttendanceRecordsBo;
import com.klp.common.core.page.TableDataInfo;
import com.klp.common.core.domain.PageQuery;
import javax.servlet.http.HttpServletResponse;
import java.util.Collection;
import java.util.Date;
import java.util.List;
@@ -28,4 +29,9 @@ public interface IAttendanceRecordsService {
* 按员工姓名集合 + 时间范围精确查询打卡记录eq/in走索引
*/
List<AttendanceRecords> queryListByEnamesAndDateRange(List<String> enames, Date startTime, Date endTime);
/**
* 导出考勤记录表(复杂格式)
*/
void exportReport(String startTime, String endTime, String pin, String ename, String deptname, HttpServletResponse response);
}

View File

@@ -2,7 +2,9 @@ package com.klp.service;
import com.klp.domain.WmsCoilQualityRejudge;
import com.klp.domain.vo.WmsCoilQualityRejudgeVo;
import com.klp.domain.vo.WmsMaterialCoilRejudgeVo;
import com.klp.domain.bo.WmsCoilQualityRejudgeBo;
import com.klp.domain.bo.WmsMaterialCoilBo;
import com.klp.common.core.page.TableDataInfo;
import com.klp.common.core.domain.PageQuery;
@@ -46,4 +48,13 @@ public interface IWmsCoilQualityRejudgeService {
* 校验并批量删除钢卷质量改判记录信息
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 查询钢卷列表并附带质量改判记录
* 调用 IWmsMaterialCoilService.queryList 查询钢卷,然后批量查询每个钢卷的最新改判记录并回填
*
* @param bo 钢卷查询条件
* @return 钢卷列表(每个钢卷附带最新一条改判记录)
*/
List<WmsMaterialCoilRejudgeVo> queryCoilListWithRejudge(WmsMaterialCoilBo bo);
}

View File

@@ -8,6 +8,8 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.klp.common.utils.StringUtils;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import com.klp.domain.bo.AttendanceRecordsBo;
import com.klp.domain.vo.AttendanceRecordsVo;
@@ -15,14 +17,23 @@ import com.klp.domain.AttendanceRecords;
import com.klp.mapper.AttendanceRecordsMapper;
import com.klp.service.IAttendanceRecordsService;
import java.util.Date;
import java.util.List;
import java.util.Collection;
import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
@RequiredArgsConstructor
@Service
public class AttendanceRecordsServiceImpl implements IAttendanceRecordsService {
private static final Logger log = LoggerFactory.getLogger(AttendanceRecordsServiceImpl.class);
private final AttendanceRecordsMapper baseMapper;
@Override
@@ -91,4 +102,246 @@ public class AttendanceRecordsServiceImpl implements IAttendanceRecordsService {
.ge(AttendanceRecords::getChecktime, startTime)
.le(AttendanceRecords::getChecktime, endTime));
}
@Override
public void exportReport(String startTime, String endTime, String pin, String ename, String deptname, HttpServletResponse response) {
try {
// 1. 解析时间范围
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate startDate = LocalDate.parse(startTime, fmt);
LocalDate endDate = LocalDate.parse(endTime, fmt);
int totalDays = startDate.lengthOfMonth();
Date startDatetime = java.sql.Timestamp.valueOf(startDate.atStartOfDay());
Date endDatetime = java.sql.Timestamp.valueOf(endDate.plusDays(1).atStartOfDay().minusNanos(1));
// 2. 构建查询
LambdaQueryWrapper<AttendanceRecords> lqw = Wrappers.lambdaQuery();
lqw.ge(AttendanceRecords::getChecktime, startDatetime);
lqw.le(AttendanceRecords::getChecktime, endDatetime);
if (StringUtils.isNotBlank(pin)) {
lqw.eq(AttendanceRecords::getPin, pin);
}
if (StringUtils.isNotBlank(ename)) {
lqw.like(AttendanceRecords::getEname, ename);
}
if (StringUtils.isNotBlank(deptname)) {
lqw.eq(AttendanceRecords::getDeptname, deptname);
}
lqw.orderByAsc(AttendanceRecords::getPin).orderByAsc(AttendanceRecords::getChecktime);
List<AttendanceRecords> records = baseMapper.selectList(lqw);
// 3. 按员工分组,再按日期分组
// key: pin, value: 员工信息 + 按天分组的打卡记录
LinkedHashMap<String, EmployeeAttendance> employeeMap = new LinkedHashMap<>();
SimpleDateFormat timeFmt = new SimpleDateFormat("HH:mm");
for (AttendanceRecords r : records) {
String pinKey = r.getPin();
EmployeeAttendance ea = employeeMap.computeIfAbsent(pinKey, k -> {
EmployeeAttendance e = new EmployeeAttendance();
e.pin = r.getPin();
e.ename = r.getEname();
e.deptname = r.getDeptname();
e.dailyRecords = new TreeMap<>();
return e;
});
LocalDate checkDate = r.getChecktime().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate();
int dayOfMonth = checkDate.getDayOfMonth();
String timeStr = timeFmt.format(r.getChecktime());
ea.dailyRecords.merge(dayOfMonth, new ArrayList<>(Collections.singletonList(timeStr)), (old, nw) -> {
old.addAll(nw);
return old;
});
}
// 4. 创建 Excel
SXSSFWorkbook wb = new SXSSFWorkbook(500);
Sheet sheet = wb.createSheet("考勤记录");
int currentRow = 0;
// 样式
CellStyle titleStyle = createTitleStyle(wb);
CellStyle headerStyle = createHeaderStyle(wb);
CellStyle dayHeaderStyle = createDayHeaderStyle(wb);
CellStyle infoStyle = createInfoStyle(wb);
CellStyle detailStyle = createDetailStyle(wb);
// --- Row 0: 考勤记录表 ---
Row titleRow = sheet.createRow(currentRow++);
titleRow.setHeightInPoints(36);
Cell titleCell = titleRow.createCell(0);
titleCell.setCellValue("考勤记录表");
titleCell.setCellStyle(titleStyle);
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, totalDays + 2));
// --- Row 1: 考勤时间 + 制表时间 ---
Row timeRow = sheet.createRow(currentRow++);
timeRow.setHeightInPoints(22);
Cell timeLabelCell = timeRow.createCell(0);
timeLabelCell.setCellValue("考勤时间");
timeLabelCell.setCellStyle(infoStyle);
Cell timeValueCell = timeRow.createCell(1);
String periodStr = startDate.format(fmt) + " ~ " + endDate.format(fmt);
timeValueCell.setCellValue(periodStr);
timeValueCell.setCellStyle(infoStyle);
Cell makeLabelCell = timeRow.createCell(totalDays / 2 + 1);
makeLabelCell.setCellValue("制表时间");
makeLabelCell.setCellStyle(infoStyle);
Cell makeValueCell = timeRow.createCell(totalDays / 2 + 2);
makeValueCell.setCellValue(LocalDate.now().format(fmt));
makeValueCell.setCellStyle(infoStyle);
// --- Row 2: 日期列头 1,2,3,... ---
Row dayHeaderRow = sheet.createRow(currentRow++);
dayHeaderRow.setHeightInPoints(20);
Cell dayCell = dayHeaderRow.createCell(0);
dayCell.setCellStyle(dayHeaderStyle);
for (int d = 1; d <= totalDays; d++) {
Cell c = dayHeaderRow.createCell(d);
c.setCellValue(d);
c.setCellStyle(dayHeaderStyle);
}
// --- 员工循环 ---
int dataColCount = totalDays + 1;
for (EmployeeAttendance ea : employeeMap.values()) {
// 员工信息行
Row infoRow = sheet.createRow(currentRow++);
infoRow.setHeightInPoints(22);
Cell pinLabel = infoRow.createCell(0);
pinLabel.setCellValue("工号:");
pinLabel.setCellStyle(infoStyle);
Cell pinValue = infoRow.createCell(1);
pinValue.setCellValue(ea.pin != null ? ea.pin : "");
pinValue.setCellStyle(infoStyle);
int nameCol = Math.max(3, totalDays / 4);
Cell nameLabel = infoRow.createCell(nameCol);
nameLabel.setCellValue("姓 名:");
nameLabel.setCellStyle(infoStyle);
Cell nameValue = infoRow.createCell(nameCol + 1);
nameValue.setCellValue(ea.ename != null ? ea.ename : "");
nameValue.setCellStyle(infoStyle);
int deptCol = Math.max(nameCol + 3, totalDays / 2);
Cell deptLabel = infoRow.createCell(deptCol);
deptLabel.setCellValue("部 门:");
deptLabel.setCellStyle(infoStyle);
Cell deptValue = infoRow.createCell(deptCol + 1);
deptValue.setCellValue(ea.deptname != null ? ea.deptname : "");
deptValue.setCellStyle(infoStyle);
// 打卡明细行
Row detailRow = sheet.createRow(currentRow++);
detailRow.setHeightInPoints(18 * 4);
for (int d = 1; d <= totalDays; d++) {
Cell c = detailRow.createCell(d);
List<String> times = ea.dailyRecords.get(d);
if (times != null && !times.isEmpty()) {
c.setCellValue(String.join("\n", times));
}
c.setCellStyle(detailStyle);
}
// 最后一行不设边框(区分下一个员工),但保留整体边框
}
// 设置列宽
sheet.setColumnWidth(0, 2000);
for (int d = 1; d <= totalDays; d++) {
sheet.setColumnWidth(d, 2400);
}
// 5. 输出
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = "考勤记录表_" + startTime + "_" + endTime;
String encodedFileName = java.net.URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-Disposition", "attachment;filename=" + encodedFileName + ".xlsx");
OutputStream out = response.getOutputStream();
wb.write(out);
out.flush();
wb.close();
} catch (Exception e) {
log.error("导出考勤记录表失败", e);
throw new RuntimeException("导出考勤记录表失败: " + e.getMessage());
}
}
private CellStyle createTitleStyle(SXSSFWorkbook wb) {
CellStyle style = wb.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
Font font = wb.createFont();
font.setFontName("宋体");
font.setFontHeightInPoints((short) 18);
font.setBold(true);
style.setFont(font);
return style;
}
private CellStyle createHeaderStyle(SXSSFWorkbook wb) {
CellStyle style = wb.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
style.setBorderBottom(BorderStyle.THIN);
Font font = wb.createFont();
font.setFontName("宋体");
font.setFontHeightInPoints((short) 10);
style.setFont(font);
return style;
}
private CellStyle createDayHeaderStyle(SXSSFWorkbook wb) {
CellStyle style = wb.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderTop(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
Font font = wb.createFont();
font.setFontName("宋体");
font.setFontHeightInPoints((short) 10);
style.setFont(font);
return style;
}
private CellStyle createInfoStyle(SXSSFWorkbook wb) {
CellStyle style = wb.createCellStyle();
style.setAlignment(HorizontalAlignment.LEFT);
style.setVerticalAlignment(VerticalAlignment.CENTER);
style.setBorderBottom(BorderStyle.THIN);
Font font = wb.createFont();
font.setFontName("宋体");
font.setFontHeightInPoints((short) 10);
style.setFont(font);
return style;
}
private CellStyle createDetailStyle(SXSSFWorkbook wb) {
CellStyle style = wb.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.TOP);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderTop(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setWrapText(true);
Font font = wb.createFont();
font.setFontName("宋体");
font.setFontHeightInPoints((short) 9);
style.setFont(font);
return style;
}
private static class EmployeeAttendance {
String pin;
String ename;
String deptname;
// key: dayOfMonth(1~31), value: 当天打卡时间列表
TreeMap<Integer, List<String>> dailyRecords;
}
}

View File

@@ -10,14 +10,17 @@ import com.klp.common.utils.StringUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import com.klp.domain.bo.WmsCoilQualityRejudgeBo;
import com.klp.domain.bo.WmsMaterialCoilBo;
import com.klp.domain.vo.WmsCoilQualityRejudgeVo;
import com.klp.domain.vo.WmsMaterialCoilRejudgeVo;
import com.klp.domain.vo.WmsMaterialCoilVo;
import com.klp.domain.WmsCoilQualityRejudge;
import com.klp.mapper.WmsCoilQualityRejudgeMapper;
import com.klp.service.IWmsCoilQualityRejudgeService;
import com.klp.service.IWmsMaterialCoilService;
import java.util.List;
import java.util.Map;
import java.util.Collection;
import java.util.*;
import java.util.stream.Collectors;
/**
* 钢卷质量改判记录Service业务层处理
@@ -30,6 +33,7 @@ import java.util.Collection;
public class WmsCoilQualityRejudgeServiceImpl implements IWmsCoilQualityRejudgeService {
private final WmsCoilQualityRejudgeMapper baseMapper;
private final IWmsMaterialCoilService wmsMaterialCoilService;
/**
* 查询钢卷质量改判记录
@@ -99,6 +103,53 @@ public class WmsCoilQualityRejudgeServiceImpl implements IWmsCoilQualityRejudgeS
//TODO 做一些数据校验,如唯一约束
}
/**
* 查询钢卷列表并附带质量改判记录
*/
@Override
public List<WmsMaterialCoilRejudgeVo> queryCoilListWithRejudge(WmsMaterialCoilBo bo) {
List<WmsMaterialCoilVo> coils = wmsMaterialCoilService.queryList(bo);
if (coils == null || coils.isEmpty()) {
return Collections.emptyList();
}
// 收集所有钢卷ID
List<Long> coilIds = coils.stream()
.map(WmsMaterialCoilVo::getCoilId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (coilIds.isEmpty()) {
return coils.stream().map(vo -> {
WmsMaterialCoilRejudgeVo rvo = new WmsMaterialCoilRejudgeVo();
BeanUtil.copyProperties(vo, rvo);
return rvo;
}).collect(Collectors.toList());
}
// 批量查询每个钢卷的最新一条改判记录
List<WmsCoilQualityRejudgeVo> latestRejudges = baseMapper.selectLatestByCoilIds(coilIds);
Map<Long, WmsCoilQualityRejudgeVo> rejudgeMap = new HashMap<>();
if (latestRejudges != null && !latestRejudges.isEmpty()) {
latestRejudges.forEach(r -> {
if (r.getCoilId() != null) {
rejudgeMap.put(r.getCoilId(), r);
}
});
}
// 组装返回结果
return coils.stream().map(vo -> {
WmsMaterialCoilRejudgeVo rvo = new WmsMaterialCoilRejudgeVo();
BeanUtil.copyProperties(vo, rvo);
if (vo.getCoilId() != null && rejudgeMap.containsKey(vo.getCoilId())) {
rvo.setRejudgeInfo(rejudgeMap.get(vo.getCoilId()));
}
return rvo;
}).collect(Collectors.toList());
}
/**
* 批量删除钢卷质量改判记录
*/