Compare commits
2 Commits
7737fda061
...
e76c07fb5e
| Author | SHA1 | Date | |
|---|---|---|---|
| e76c07fb5e | |||
| d872e3e235 |
@ -0,0 +1,80 @@
|
|||||||
|
package com.ruoyi.web.controller.warehouse;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import com.ruoyi.common.annotation.Log;
|
||||||
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
|
import com.ruoyi.common.core.page.TableDataInfo;
|
||||||
|
import com.ruoyi.common.enums.BusinessType;
|
||||||
|
import com.ruoyi.warehouse.domain.WmsBatchActionUpdateRequest;
|
||||||
|
import com.ruoyi.warehouse.domain.WmsBatchItem;
|
||||||
|
import com.ruoyi.warehouse.domain.WmsBatchJob;
|
||||||
|
import com.ruoyi.warehouse.service.IWmsBatchImportService;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/warehouse/batch")
|
||||||
|
public class WmsBatchImportController extends BaseController
|
||||||
|
{
|
||||||
|
@Autowired
|
||||||
|
private IWmsBatchImportService batchImportService;
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
public TableDataInfo list(WmsBatchJob job)
|
||||||
|
{
|
||||||
|
startPage();
|
||||||
|
return getDataTable(batchImportService.selectJobList(job));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/upload")
|
||||||
|
@Log(title = "WMS批量导入", businessType = BusinessType.IMPORT)
|
||||||
|
public AjaxResult upload(MultipartFile file) throws Exception
|
||||||
|
{
|
||||||
|
return success(batchImportService.uploadExcel(file, getUsername()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public AjaxResult getInfo(@PathVariable Long id)
|
||||||
|
{
|
||||||
|
return success(batchImportService.selectJobById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{jobId}/items")
|
||||||
|
public AjaxResult items(@PathVariable Long jobId)
|
||||||
|
{
|
||||||
|
return success(batchImportService.selectItemList(jobId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/item")
|
||||||
|
public AjaxResult updateItem(@RequestBody WmsBatchItem item)
|
||||||
|
{
|
||||||
|
return success(batchImportService.updateItem(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{jobId}/items/action")
|
||||||
|
public AjaxResult updateItemAction(@PathVariable Long jobId, @RequestBody WmsBatchActionUpdateRequest request)
|
||||||
|
{
|
||||||
|
return toAjax(batchImportService.updateItemAction(jobId, request.getActionType(), request.getItemIds()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/item/{id}")
|
||||||
|
public AjaxResult deleteItem(@PathVariable Long id)
|
||||||
|
{
|
||||||
|
return toAjax(batchImportService.deleteItem(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{jobId}/confirm")
|
||||||
|
@Log(title = "WMS批量确认执行", businessType = BusinessType.INSERT)
|
||||||
|
public AjaxResult confirm(@PathVariable Long jobId)
|
||||||
|
{
|
||||||
|
return success(batchImportService.confirmJob(jobId, getUsername()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,13 +6,12 @@ ruoyi:
|
|||||||
version: 3.9.2
|
version: 3.9.2
|
||||||
# 版权年份
|
# 版权年份
|
||||||
copyrightYear: 2026
|
copyrightYear: 2026
|
||||||
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
|
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux/Docker配置 /app/uploadPath)
|
||||||
profile: D:/ruoyi/uploadPath
|
profile: ${RUOYI_PROFILE:D:/ruoyi/uploadPath}
|
||||||
# 获取ip地址开关
|
# 获取ip地址开关
|
||||||
addressEnabled: false
|
addressEnabled: false
|
||||||
# 验证码类型 math 数字计算 char 字符验证
|
# 验证码类型 math 数字计算 char 字符验证
|
||||||
captchaType: math
|
captchaType: math
|
||||||
|
|
||||||
# 开发环境配置
|
# 开发环境配置
|
||||||
server:
|
server:
|
||||||
# 服务器的HTTP端口,默认为8080
|
# 服务器的HTTP端口,默认为8080
|
||||||
@ -57,9 +56,9 @@ spring:
|
|||||||
servlet:
|
servlet:
|
||||||
multipart:
|
multipart:
|
||||||
# 单个文件大小
|
# 单个文件大小
|
||||||
max-file-size: 10MB
|
max-file-size: ${MAX_FILE_SIZE:50MB}
|
||||||
# 设置总上传的文件大小
|
# 设置总上传的文件大小
|
||||||
max-request-size: 20MB
|
max-request-size: ${MAX_REQUEST_SIZE:60MB}
|
||||||
# 服务模块
|
# 服务模块
|
||||||
devtools:
|
devtools:
|
||||||
restart:
|
restart:
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
package com.ruoyi.warehouse.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class WmsBatchActionUpdateRequest
|
||||||
|
{
|
||||||
|
private String actionType;
|
||||||
|
private List<Long> itemIds;
|
||||||
|
|
||||||
|
public String getActionType()
|
||||||
|
{
|
||||||
|
return actionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActionType(String actionType)
|
||||||
|
{
|
||||||
|
this.actionType = actionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Long> getItemIds()
|
||||||
|
{
|
||||||
|
return itemIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setItemIds(List<Long> itemIds)
|
||||||
|
{
|
||||||
|
this.itemIds = itemIds;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
package com.ruoyi.warehouse.domain;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import com.ruoyi.common.core.domain.BaseEntity;
|
||||||
|
|
||||||
|
public class WmsBatchItem extends BaseEntity
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private Long jobId;
|
||||||
|
private Integer rowNo;
|
||||||
|
private String actionType;
|
||||||
|
private String rawText;
|
||||||
|
private String parsedJson;
|
||||||
|
private Integer status;
|
||||||
|
private String errorMsg;
|
||||||
|
private String warningMsg;
|
||||||
|
private Long refId;
|
||||||
|
private String areaName;
|
||||||
|
private String productName;
|
||||||
|
private String brand;
|
||||||
|
private String category;
|
||||||
|
private String spec;
|
||||||
|
private String unit;
|
||||||
|
private BigDecimal quantity;
|
||||||
|
private String operatorName;
|
||||||
|
private Integer typeCode;
|
||||||
|
private String sourceNo;
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
public Long getJobId() { return jobId; }
|
||||||
|
public void setJobId(Long jobId) { this.jobId = jobId; }
|
||||||
|
public Integer getRowNo() { return rowNo; }
|
||||||
|
public void setRowNo(Integer rowNo) { this.rowNo = rowNo; }
|
||||||
|
public String getActionType() { return actionType; }
|
||||||
|
public void setActionType(String actionType) { this.actionType = actionType; }
|
||||||
|
public String getRawText() { return rawText; }
|
||||||
|
public void setRawText(String rawText) { this.rawText = rawText; }
|
||||||
|
public String getParsedJson() { return parsedJson; }
|
||||||
|
public void setParsedJson(String parsedJson) { this.parsedJson = parsedJson; }
|
||||||
|
public Integer getStatus() { return status; }
|
||||||
|
public void setStatus(Integer status) { this.status = status; }
|
||||||
|
public String getErrorMsg() { return errorMsg; }
|
||||||
|
public void setErrorMsg(String errorMsg) { this.errorMsg = errorMsg; }
|
||||||
|
public String getWarningMsg() { return warningMsg; }
|
||||||
|
public void setWarningMsg(String warningMsg) { this.warningMsg = warningMsg; }
|
||||||
|
public Long getRefId() { return refId; }
|
||||||
|
public void setRefId(Long refId) { this.refId = refId; }
|
||||||
|
public String getAreaName() { return areaName; }
|
||||||
|
public void setAreaName(String areaName) { this.areaName = areaName; }
|
||||||
|
public String getProductName() { return productName; }
|
||||||
|
public void setProductName(String productName) { this.productName = productName; }
|
||||||
|
public String getBrand() { return brand; }
|
||||||
|
public void setBrand(String brand) { this.brand = brand; }
|
||||||
|
public String getCategory() { return category; }
|
||||||
|
public void setCategory(String category) { this.category = category; }
|
||||||
|
public String getSpec() { return spec; }
|
||||||
|
public void setSpec(String spec) { this.spec = spec; }
|
||||||
|
public String getUnit() { return unit; }
|
||||||
|
public void setUnit(String unit) { this.unit = unit; }
|
||||||
|
public BigDecimal getQuantity() { return quantity; }
|
||||||
|
public void setQuantity(BigDecimal quantity) { this.quantity = quantity; }
|
||||||
|
public String getOperatorName() { return operatorName; }
|
||||||
|
public void setOperatorName(String operatorName) { this.operatorName = operatorName; }
|
||||||
|
public Integer getTypeCode() { return typeCode; }
|
||||||
|
public void setTypeCode(Integer typeCode) { this.typeCode = typeCode; }
|
||||||
|
public String getSourceNo() { return sourceNo; }
|
||||||
|
public void setSourceNo(String sourceNo) { this.sourceNo = sourceNo; }
|
||||||
|
public String getRemark() { return remark; }
|
||||||
|
public void setRemark(String remark) { this.remark = remark; }
|
||||||
|
}
|
||||||
@ -0,0 +1,141 @@
|
|||||||
|
package com.ruoyi.warehouse.domain;
|
||||||
|
|
||||||
|
import com.ruoyi.common.core.domain.BaseEntity;
|
||||||
|
|
||||||
|
public class WmsBatchJob extends BaseEntity
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private String jobNo;
|
||||||
|
private String sourceType;
|
||||||
|
private String bizType;
|
||||||
|
private String fileName;
|
||||||
|
private String filePath;
|
||||||
|
private Integer status;
|
||||||
|
private Integer progress;
|
||||||
|
private Integer totalCount;
|
||||||
|
private Integer successCount;
|
||||||
|
private Integer failCount;
|
||||||
|
private String errorMsg;
|
||||||
|
|
||||||
|
public Long getId()
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getJobNo()
|
||||||
|
{
|
||||||
|
return jobNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setJobNo(String jobNo)
|
||||||
|
{
|
||||||
|
this.jobNo = jobNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSourceType()
|
||||||
|
{
|
||||||
|
return sourceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSourceType(String sourceType)
|
||||||
|
{
|
||||||
|
this.sourceType = sourceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBizType()
|
||||||
|
{
|
||||||
|
return bizType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBizType(String bizType)
|
||||||
|
{
|
||||||
|
this.bizType = bizType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFileName()
|
||||||
|
{
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFileName(String fileName)
|
||||||
|
{
|
||||||
|
this.fileName = fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFilePath()
|
||||||
|
{
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFilePath(String filePath)
|
||||||
|
{
|
||||||
|
this.filePath = filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getStatus()
|
||||||
|
{
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(Integer status)
|
||||||
|
{
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getProgress()
|
||||||
|
{
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProgress(Integer progress)
|
||||||
|
{
|
||||||
|
this.progress = progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getTotalCount()
|
||||||
|
{
|
||||||
|
return totalCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalCount(Integer totalCount)
|
||||||
|
{
|
||||||
|
this.totalCount = totalCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getSuccessCount()
|
||||||
|
{
|
||||||
|
return successCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSuccessCount(Integer successCount)
|
||||||
|
{
|
||||||
|
this.successCount = successCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getFailCount()
|
||||||
|
{
|
||||||
|
return failCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFailCount(Integer failCount)
|
||||||
|
{
|
||||||
|
this.failCount = failCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getErrorMsg()
|
||||||
|
{
|
||||||
|
return errorMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setErrorMsg(String errorMsg)
|
||||||
|
{
|
||||||
|
this.errorMsg = errorMsg;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package com.ruoyi.warehouse.mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import com.ruoyi.warehouse.domain.WmsBatchItem;
|
||||||
|
|
||||||
|
public interface WmsBatchItemMapper
|
||||||
|
{
|
||||||
|
public List<WmsBatchItem> selectWmsBatchItemList(WmsBatchItem item);
|
||||||
|
public WmsBatchItem selectWmsBatchItemById(Long id);
|
||||||
|
public int insertWmsBatchItem(WmsBatchItem item);
|
||||||
|
public int updateWmsBatchItem(WmsBatchItem item);
|
||||||
|
public int deleteWmsBatchItemById(Long id);
|
||||||
|
public int deleteWmsBatchItemByJobId(Long jobId);
|
||||||
|
public int countByJobIdAndStatus(WmsBatchItem item);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package com.ruoyi.warehouse.mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import com.ruoyi.warehouse.domain.WmsBatchJob;
|
||||||
|
|
||||||
|
public interface WmsBatchJobMapper
|
||||||
|
{
|
||||||
|
public List<WmsBatchJob> selectWmsBatchJobList(WmsBatchJob job);
|
||||||
|
public WmsBatchJob selectWmsBatchJobById(Long id);
|
||||||
|
public int insertWmsBatchJob(WmsBatchJob job);
|
||||||
|
public int updateWmsBatchJob(WmsBatchJob job);
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ public interface IProductInfoService
|
|||||||
public List<ProductInfo> selectProductInfoAll();
|
public List<ProductInfo> selectProductInfoAll();
|
||||||
public ProductInfo selectProductInfoById(Long id);
|
public ProductInfo selectProductInfoById(Long id);
|
||||||
public boolean checkProductCodeUnique(ProductInfo productInfo);
|
public boolean checkProductCodeUnique(ProductInfo productInfo);
|
||||||
|
public String inferProductCategory(ProductInfo productInfo);
|
||||||
public int insertProductInfo(ProductInfo productInfo);
|
public int insertProductInfo(ProductInfo productInfo);
|
||||||
public int updateProductInfo(ProductInfo productInfo);
|
public int updateProductInfo(ProductInfo productInfo);
|
||||||
public int deleteProductInfoByIds(Long[] ids);
|
public int deleteProductInfoByIds(Long[] ids);
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
package com.ruoyi.warehouse.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import com.ruoyi.warehouse.domain.WmsBatchItem;
|
||||||
|
import com.ruoyi.warehouse.domain.WmsBatchJob;
|
||||||
|
|
||||||
|
public interface IWmsBatchImportService
|
||||||
|
{
|
||||||
|
public WmsBatchJob uploadExcel(MultipartFile file, String username) throws Exception;
|
||||||
|
public List<WmsBatchJob> selectJobList(WmsBatchJob job);
|
||||||
|
public WmsBatchJob selectJobById(Long id);
|
||||||
|
public List<WmsBatchItem> selectItemList(Long jobId);
|
||||||
|
public WmsBatchItem updateItem(WmsBatchItem item);
|
||||||
|
public int updateItemAction(Long jobId, String actionType, List<Long> itemIds);
|
||||||
|
public int deleteItem(Long id);
|
||||||
|
public WmsBatchJob confirmJob(Long jobId, String username);
|
||||||
|
}
|
||||||
@ -9,8 +9,10 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@ -258,8 +260,8 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
}
|
}
|
||||||
if (ACTION_CREATE_OUT.equals(plan.getAction()) && plan.getOutType() == null)
|
if (ACTION_CREATE_OUT.equals(plan.getAction()) && plan.getOutType() == null)
|
||||||
{
|
{
|
||||||
plan.setOutType(3);
|
plan.setOutType(1);
|
||||||
plan.getWarnings().add("未识别出库类型,默认销售出库");
|
plan.getWarnings().add("未识别出库类型,默认生产出库");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ACTION_CONFIRM_IN.equals(plan.getAction()))
|
if (ACTION_CONFIRM_IN.equals(plan.getAction()))
|
||||||
@ -274,7 +276,7 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
fillStockResults(plan);
|
fillStockResults(plan);
|
||||||
plan.setNeedConfirm(false);
|
plan.setNeedConfirm(false);
|
||||||
plan.setExecutable(true);
|
plan.setExecutable(true);
|
||||||
plan.setMessage(plan.getResults().isEmpty() ? "未查询到库存" : "查询到 " + plan.getResults().size() + " 条库存");
|
plan.setMessage(plan.getResults().isEmpty() ? stockNotFoundMessage(plan) : "查询到 " + plan.getResults().size() + " 条库存");
|
||||||
if (StringUtils.isBlank(plan.getSummary()))
|
if (StringUtils.isBlank(plan.getSummary()))
|
||||||
{
|
{
|
||||||
plan.setSummary("查询库存");
|
plan.setSummary("查询库存");
|
||||||
@ -312,7 +314,7 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
fillSummary(plan);
|
fillSummary(plan);
|
||||||
if (!plan.isExecutable() && StringUtils.isBlank(plan.getMessage()))
|
if (!plan.isExecutable() && StringUtils.isBlank(plan.getMessage()))
|
||||||
{
|
{
|
||||||
plan.setMessage("信息不完整或存在多个候选,请补充后再执行");
|
fillUnableToExecuteMessage(plan);
|
||||||
}
|
}
|
||||||
return plan;
|
return plan;
|
||||||
}
|
}
|
||||||
@ -418,7 +420,8 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
+ "如果用户说第几个、第二个、第2条等,referenceIndex 返回从 1 开始的序号。"
|
+ "如果用户说第几个、第二个、第2条等,referenceIndex 返回从 1 开始的序号。"
|
||||||
+ "入库类型 inType:1生产入库,2采购入库,3归还入库,4退货入库。"
|
+ "入库类型 inType:1生产入库,2采购入库,3归还入库,4退货入库。"
|
||||||
+ "出库类型 outType:1生产出库,2退货出库,3销售出库。"
|
+ "出库类型 outType:1生产出库,2退货出库,3销售出库。"
|
||||||
+ "新增产品时尽量从用户输入提取 productName, brand, category, spec, unit;新增库区提取 areaName。不要编造信息;无法识别就填 null。";
|
+ "新增产品时尽量从用户输入提取 productName, brand, category, spec, unit;如果用户未提供 category,优先从 existingCategories 选择合适类别,不合适时推断一个简短合理的类别。"
|
||||||
|
+ "除 category 可按规则推断外,不要编造其他字段;无法识别就填 null。新增库区提取 areaName。";
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildUserInput(String text, List<AgentResultItem> contextResults)
|
private String buildUserInput(String text, List<AgentResultItem> contextResults)
|
||||||
@ -426,6 +429,7 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
JSONObject payload = new JSONObject();
|
JSONObject payload = new JSONObject();
|
||||||
payload.put("text", text);
|
payload.put("text", text);
|
||||||
payload.put("contextResults", contextResults == null ? new ArrayList<>() : contextResults);
|
payload.put("contextResults", contextResults == null ? new ArrayList<>() : contextResults);
|
||||||
|
payload.put("existingCategories", existingCategories());
|
||||||
return payload.toJSONString();
|
return payload.toJSONString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -850,7 +854,13 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
ProductInfo product = match.getProduct();
|
ProductInfo product = match.getProduct();
|
||||||
plan.getCandidates().add(new AgentCandidate(product.getId(), "product", product.getProductName(), productDetail(product)));
|
plan.getCandidates().add(new AgentCandidate(product.getId(), "product", product.getProductName(), productDetail(product)));
|
||||||
}
|
}
|
||||||
plan.setMessage("产品不唯一,请补充名称、规格、品牌或类别");
|
if (!ACTION_QUERY_STOCK.equals(plan.getAction()))
|
||||||
|
{
|
||||||
|
fillProductChoices(plan, topMatches);
|
||||||
|
}
|
||||||
|
plan.setMessage(plan.getResults().isEmpty()
|
||||||
|
? "产品不唯一,请选择候选产品,或补充名称、规格、品牌、类别"
|
||||||
|
: "找到多个匹配产品,请在下方选择要操作的库存项");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resolveReferencedResult(AgentPlan plan, String originalText, List<AgentResultItem> contextResults)
|
private void resolveReferencedResult(AgentPlan plan, String originalText, List<AgentResultItem> contextResults)
|
||||||
@ -965,6 +975,37 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
{
|
{
|
||||||
plan.setUnit(plan.getUnit().trim());
|
plan.setUnit(plan.getUnit().trim());
|
||||||
}
|
}
|
||||||
|
if (StringUtils.isBlank(plan.getCategory()) && StringUtils.isNotBlank(plan.getProductName()))
|
||||||
|
{
|
||||||
|
ProductInfo draft = new ProductInfo();
|
||||||
|
draft.setProductName(plan.getProductName());
|
||||||
|
draft.setBrand(plan.getBrand());
|
||||||
|
draft.setSpec(plan.getSpec());
|
||||||
|
draft.setUnit(plan.getUnit());
|
||||||
|
String category = productService.inferProductCategory(draft);
|
||||||
|
if (StringUtils.isNotBlank(category))
|
||||||
|
{
|
||||||
|
plan.setCategory(category);
|
||||||
|
plan.getWarnings().add("未提供类别,已自动分类为:" + category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> existingCategories()
|
||||||
|
{
|
||||||
|
Set<String> categories = new LinkedHashSet<>();
|
||||||
|
for (ProductInfo product : productService.selectProductInfoAll())
|
||||||
|
{
|
||||||
|
if (StringUtils.isNotBlank(product.getCategory()))
|
||||||
|
{
|
||||||
|
categories.add(product.getCategory().trim());
|
||||||
|
}
|
||||||
|
if (categories.size() >= 80)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ArrayList<>(categories);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean requiresOperator(String action)
|
private boolean requiresOperator(String action)
|
||||||
@ -987,6 +1028,80 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void fillUnableToExecuteMessage(AgentPlan plan)
|
||||||
|
{
|
||||||
|
if (plan.getProductId() == null && hasProductClue(plan) && noProductCandidates(plan))
|
||||||
|
{
|
||||||
|
plan.setMessage("没找到产品:" + productClue(plan) + "。请先确认产品名称、品牌、类别或规格是否已维护。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (plan.getAreaId() == null && StringUtils.isNotBlank(plan.getAreaName()) && noAreaCandidates(plan))
|
||||||
|
{
|
||||||
|
plan.setMessage("没找到库区:" + plan.getAreaName() + "。请先维护库区后再操作。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ACTION_CREATE_OUT.equals(plan.getAction()) && plan.getProductId() != null && plan.getResults().isEmpty())
|
||||||
|
{
|
||||||
|
String product = StringUtils.defaultIfBlank(plan.getProductName(), "该产品");
|
||||||
|
plan.setMessage("没找到可出库库存:" + product + "。请确认产品是否已有库存,或选择正确库区。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (plan.getCandidates() != null && !plan.getCandidates().isEmpty())
|
||||||
|
{
|
||||||
|
plan.setMessage("存在多个候选,请先选择要操作的数据");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!validQuantity(plan))
|
||||||
|
{
|
||||||
|
plan.setMessage(ACTION_CREATE_CHECK.equals(plan.getAction()) ? "未识别到实际盘点数量" : "未识别到有效数量");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plan.setMessage("信息不完整,请补充后再执行");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean noProductCandidates(AgentPlan plan)
|
||||||
|
{
|
||||||
|
for (AgentCandidate candidate : plan.getCandidates())
|
||||||
|
{
|
||||||
|
if ("product".equals(candidate.getType()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasProductClue(AgentPlan plan)
|
||||||
|
{
|
||||||
|
return StringUtils.isNotBlank(plan.getProductName())
|
||||||
|
|| StringUtils.isNotBlank(plan.getBrand())
|
||||||
|
|| StringUtils.isNotBlank(plan.getCategory())
|
||||||
|
|| StringUtils.isNotBlank(plan.getSpec());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String productClue(AgentPlan plan)
|
||||||
|
{
|
||||||
|
return firstNotBlank(plan.getProductName(), plan.getBrand(), plan.getCategory(), plan.getSpec(), "未识别产品");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String stockNotFoundMessage(AgentPlan plan)
|
||||||
|
{
|
||||||
|
List<String> parts = new ArrayList<>();
|
||||||
|
if (StringUtils.isNotBlank(plan.getAreaName()))
|
||||||
|
{
|
||||||
|
parts.add("库区:" + plan.getAreaName());
|
||||||
|
}
|
||||||
|
if (hasProductClue(plan))
|
||||||
|
{
|
||||||
|
parts.add("产品:" + productClue(plan));
|
||||||
|
}
|
||||||
|
if (parts.isEmpty())
|
||||||
|
{
|
||||||
|
return "未查询到库存数据";
|
||||||
|
}
|
||||||
|
return "未查询到符合条件的库存数据(" + String.join(",", parts) + ")";
|
||||||
|
}
|
||||||
|
|
||||||
private void fillActionCandidates(AgentPlan plan)
|
private void fillActionCandidates(AgentPlan plan)
|
||||||
{
|
{
|
||||||
plan.getCandidates().add(new AgentCandidate(1L, "action", "查库存", "例如:查一下路由器库存"));
|
plan.getCandidates().add(new AgentCandidate(1L, "action", "查库存", "例如:查一下路由器库存"));
|
||||||
@ -1199,6 +1314,51 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AgentResultItem toProductResult(ProductInfo product)
|
||||||
|
{
|
||||||
|
AgentResultItem item = new AgentResultItem();
|
||||||
|
item.setProductId(product.getId());
|
||||||
|
item.setProductName(product.getProductName());
|
||||||
|
item.setBrand(product.getBrand());
|
||||||
|
item.setCategory(product.getCategory());
|
||||||
|
item.setSpec(product.getSpec());
|
||||||
|
item.setUnit(product.getUnit());
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillProductChoices(AgentPlan plan, List<ScoredProduct> matches)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
for (ScoredProduct match : first(matches, 10))
|
||||||
|
{
|
||||||
|
ProductInfo product = match.getProduct();
|
||||||
|
StockInfo query = new StockInfo();
|
||||||
|
query.setAreaId(plan.getAreaId());
|
||||||
|
query.setProductId(product.getId());
|
||||||
|
List<StockInfo> stockList = stockInfoService.selectStockInfoList(query);
|
||||||
|
if (stockList.isEmpty())
|
||||||
|
{
|
||||||
|
if (!ACTION_CREATE_OUT.equals(plan.getAction()))
|
||||||
|
{
|
||||||
|
plan.getResults().add(toProductResult(product));
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (StockInfo stock : stockList)
|
||||||
|
{
|
||||||
|
if (count++ >= 20)
|
||||||
|
{
|
||||||
|
plan.getWarnings().add("匹配结果较多,仅展示前 20 条");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plan.getResults().add(toResult(stock));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void fillCurrentStock(AgentPlan plan)
|
private void fillCurrentStock(AgentPlan plan)
|
||||||
{
|
{
|
||||||
if (plan.getAreaId() == null || plan.getProductId() == null)
|
if (plan.getAreaId() == null || plan.getProductId() == null)
|
||||||
@ -1235,7 +1395,8 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
|
|
||||||
private void suggestAlternativeStockAreas(AgentPlan plan)
|
private void suggestAlternativeStockAreas(AgentPlan plan)
|
||||||
{
|
{
|
||||||
if (!ACTION_CREATE_OUT.equals(plan.getAction()) || plan.getProductId() == null || plan.isExecutable())
|
if (!ACTION_CREATE_OUT.equals(plan.getAction()) || plan.getProductId() == null || plan.isExecutable()
|
||||||
|
|| (plan.getAreaId() == null && (StringUtils.isNotBlank(plan.getAreaName()) || !noAreaCandidates(plan))))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
package com.ruoyi.warehouse.service.impl;
|
package com.ruoyi.warehouse.service.impl;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import com.ruoyi.common.constant.UserConstants;
|
import com.ruoyi.common.constant.UserConstants;
|
||||||
@ -46,6 +49,21 @@ public class ProductInfoServiceImpl implements IProductInfoService
|
|||||||
return StringUtils.isNull(info) || info.getId().longValue() == id.longValue() ? UserConstants.UNIQUE : UserConstants.NOT_UNIQUE;
|
return StringUtils.isNull(info) || info.getId().longValue() == id.longValue() ? UserConstants.UNIQUE : UserConstants.NOT_UNIQUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String inferProductCategory(ProductInfo productInfo)
|
||||||
|
{
|
||||||
|
if (productInfo == null || StringUtils.isBlank(productInfo.getProductName()))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String category = inferCategoryFromHistory(productInfo);
|
||||||
|
if (StringUtils.isNotBlank(category))
|
||||||
|
{
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
return generateCategory(productInfo);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int insertProductInfo(ProductInfo productInfo)
|
public int insertProductInfo(ProductInfo productInfo)
|
||||||
{
|
{
|
||||||
@ -79,6 +97,10 @@ public class ProductInfoServiceImpl implements IProductInfoService
|
|||||||
productInfo.setCategory(trimToNull(productInfo.getCategory()));
|
productInfo.setCategory(trimToNull(productInfo.getCategory()));
|
||||||
productInfo.setSpec(trimToNull(productInfo.getSpec()));
|
productInfo.setSpec(trimToNull(productInfo.getSpec()));
|
||||||
productInfo.setUnit(trimToNull(productInfo.getUnit()));
|
productInfo.setUnit(trimToNull(productInfo.getUnit()));
|
||||||
|
if (StringUtils.isBlank(productInfo.getCategory()) && StringUtils.isNotBlank(productInfo.getProductName()))
|
||||||
|
{
|
||||||
|
productInfo.setCategory(inferProductCategory(productInfo));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateProductInfoUnique(ProductInfo productInfo)
|
private void validateProductInfoUnique(ProductInfo productInfo)
|
||||||
@ -95,4 +117,151 @@ public class ProductInfoServiceImpl implements IProductInfoService
|
|||||||
{
|
{
|
||||||
return StringUtils.isBlank(value) ? null : value.trim();
|
return StringUtils.isBlank(value) ? null : value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String inferCategoryFromHistory(ProductInfo productInfo)
|
||||||
|
{
|
||||||
|
Map<String, Integer> scores = new HashMap<>();
|
||||||
|
for (ProductInfo product : productInfoMapper.selectProductInfoAll())
|
||||||
|
{
|
||||||
|
if (StringUtils.isBlank(product.getCategory()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int score = categoryProductScore(productInfo, product);
|
||||||
|
if (score > 0)
|
||||||
|
{
|
||||||
|
String category = product.getCategory().trim();
|
||||||
|
scores.put(category, scores.getOrDefault(category, 0) + score);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String bestCategory = null;
|
||||||
|
int bestScore = 0;
|
||||||
|
int secondScore = 0;
|
||||||
|
for (Map.Entry<String, Integer> entry : scores.entrySet())
|
||||||
|
{
|
||||||
|
int score = entry.getValue();
|
||||||
|
if (score > bestScore)
|
||||||
|
{
|
||||||
|
secondScore = bestScore;
|
||||||
|
bestScore = score;
|
||||||
|
bestCategory = entry.getKey();
|
||||||
|
}
|
||||||
|
else if (score > secondScore)
|
||||||
|
{
|
||||||
|
secondScore = score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bestScore >= 4 && bestScore >= secondScore + 2)
|
||||||
|
{
|
||||||
|
return bestCategory;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int categoryProductScore(ProductInfo source, ProductInfo target)
|
||||||
|
{
|
||||||
|
int score = 0;
|
||||||
|
score += fieldScore(source.getProductName(), target.getProductName(), 8);
|
||||||
|
score += fieldScore(source.getBrand(), target.getBrand(), 6);
|
||||||
|
score += fieldScore(source.getSpec(), target.getSpec(), 4);
|
||||||
|
score += fieldScore(source.getUnit(), target.getUnit(), 1);
|
||||||
|
String draftText = productDraftText(source);
|
||||||
|
score += containsField(draftText, target.getProductName()) ? 4 : 0;
|
||||||
|
score += containsField(draftText, target.getBrand()) ? 2 : 0;
|
||||||
|
score += containsField(draftText, target.getSpec()) ? 2 : 0;
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int fieldScore(String input, String target, int exactScore)
|
||||||
|
{
|
||||||
|
if (StringUtils.isBlank(input) || StringUtils.isBlank(target))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (input.trim().equalsIgnoreCase(target.trim()))
|
||||||
|
{
|
||||||
|
return exactScore;
|
||||||
|
}
|
||||||
|
if (containsIgnoreCase(input, target) || containsIgnoreCase(target, input))
|
||||||
|
{
|
||||||
|
return Math.max(1, exactScore / 2);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String productDraftText(ProductInfo productInfo)
|
||||||
|
{
|
||||||
|
return StringUtils.defaultString(productInfo.getProductName()) + " "
|
||||||
|
+ StringUtils.defaultString(productInfo.getBrand()) + " "
|
||||||
|
+ StringUtils.defaultString(productInfo.getSpec()) + " "
|
||||||
|
+ StringUtils.defaultString(productInfo.getUnit());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateCategory(ProductInfo productInfo)
|
||||||
|
{
|
||||||
|
String text = productDraftText(productInfo).toLowerCase(Locale.ROOT);
|
||||||
|
if (containsAny(text, "路由器", "交换机", "网关", "ap", "wifi", "网线", "光模块", "网络"))
|
||||||
|
{
|
||||||
|
return "网络设备";
|
||||||
|
}
|
||||||
|
if (containsAny(text, "电脑", "主机", "显示器", "键盘", "鼠标", "硬盘", "内存", "打印机", "扫描仪"))
|
||||||
|
{
|
||||||
|
return "办公设备";
|
||||||
|
}
|
||||||
|
if (containsAny(text, "纸", "抽纸", "卷纸", "硒鼓", "墨盒", "墨粉", "笔", "文件夹", "订书", "胶带"))
|
||||||
|
{
|
||||||
|
return "办公耗材";
|
||||||
|
}
|
||||||
|
if (containsAny(text, "手套", "口罩", "安全帽", "护目镜", "反光", "劳保"))
|
||||||
|
{
|
||||||
|
return "劳保用品";
|
||||||
|
}
|
||||||
|
if (containsAny(text, "扳手", "螺丝刀", "钳", "锤", "钻", "工具"))
|
||||||
|
{
|
||||||
|
return "工具";
|
||||||
|
}
|
||||||
|
if (containsAny(text, "螺丝", "螺母", "垫片", "扎带", "卡扣", "五金"))
|
||||||
|
{
|
||||||
|
return "五金耗材";
|
||||||
|
}
|
||||||
|
if (containsAny(text, "传感器", "继电器", "电源", "线束", "电缆", "接头", "模块", "芯片", "电阻", "电容"))
|
||||||
|
{
|
||||||
|
return "电子元器件";
|
||||||
|
}
|
||||||
|
if (containsAny(text, "机油", "润滑", "清洗剂", "胶水", "胶粘", "油漆", "化学"))
|
||||||
|
{
|
||||||
|
return "化工耗材";
|
||||||
|
}
|
||||||
|
if (containsAny(text, "轮胎", "刹车", "电瓶", "电池", "车灯", "滤芯", "汽车"))
|
||||||
|
{
|
||||||
|
return "汽车配件";
|
||||||
|
}
|
||||||
|
return "通用物料";
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean containsAny(String text, String... keywords)
|
||||||
|
{
|
||||||
|
for (String keyword : keywords)
|
||||||
|
{
|
||||||
|
if (containsIgnoreCase(text, keyword))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean containsField(String text, String field)
|
||||||
|
{
|
||||||
|
return StringUtils.isNotBlank(text) && StringUtils.isNotBlank(field) && containsIgnoreCase(text, field);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean containsIgnoreCase(String text, String keyword)
|
||||||
|
{
|
||||||
|
if (StringUtils.isBlank(text) || StringUtils.isBlank(keyword))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return text.toLowerCase(Locale.ROOT).contains(keyword.toLowerCase(Locale.ROOT));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,98 @@
|
|||||||
|
<?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.ruoyi.warehouse.mapper.WmsBatchItemMapper">
|
||||||
|
<resultMap type="WmsBatchItem" id="WmsBatchItemResult">
|
||||||
|
<id property="id" column="id"/>
|
||||||
|
<result property="jobId" column="job_id"/>
|
||||||
|
<result property="rowNo" column="row_no"/>
|
||||||
|
<result property="actionType" column="action_type"/>
|
||||||
|
<result property="rawText" column="raw_text"/>
|
||||||
|
<result property="parsedJson" column="parsed_json"/>
|
||||||
|
<result property="status" column="status"/>
|
||||||
|
<result property="errorMsg" column="error_msg"/>
|
||||||
|
<result property="warningMsg" column="warning_msg"/>
|
||||||
|
<result property="refId" column="ref_id"/>
|
||||||
|
<result property="areaName" column="area_name"/>
|
||||||
|
<result property="productName" column="product_name"/>
|
||||||
|
<result property="brand" column="brand"/>
|
||||||
|
<result property="category" column="category"/>
|
||||||
|
<result property="spec" column="spec"/>
|
||||||
|
<result property="unit" column="unit"/>
|
||||||
|
<result property="quantity" column="quantity"/>
|
||||||
|
<result property="operatorName" column="operator_name"/>
|
||||||
|
<result property="typeCode" column="type_code"/>
|
||||||
|
<result property="sourceNo" column="source_no"/>
|
||||||
|
<result property="remark" column="remark"/>
|
||||||
|
<result property="createTime" column="create_time"/>
|
||||||
|
<result property="updateTime" column="update_time"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<sql id="selectWmsBatchItemVo">
|
||||||
|
select id, job_id, row_no, action_type, raw_text, parsed_json, status, error_msg, warning_msg, ref_id,
|
||||||
|
area_name, product_name, brand, category, spec, unit, quantity, operator_name, type_code,
|
||||||
|
source_no, remark, create_time, update_time
|
||||||
|
from wms_batch_item
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="selectWmsBatchItemList" parameterType="WmsBatchItem" resultMap="WmsBatchItemResult">
|
||||||
|
<include refid="selectWmsBatchItemVo"/>
|
||||||
|
<where>
|
||||||
|
<if test="jobId != null">and job_id = #{jobId}</if>
|
||||||
|
<if test="status != null">and status = #{status}</if>
|
||||||
|
<if test="actionType != null and actionType != ''">and action_type = #{actionType}</if>
|
||||||
|
</where>
|
||||||
|
order by row_no asc, id asc
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectWmsBatchItemById" parameterType="Long" resultMap="WmsBatchItemResult">
|
||||||
|
<include refid="selectWmsBatchItemVo"/> where id = #{id}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="countByJobIdAndStatus" parameterType="WmsBatchItem" resultType="int">
|
||||||
|
select count(1) from wms_batch_item where job_id = #{jobId} and status = #{status}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insertWmsBatchItem" parameterType="WmsBatchItem" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
insert into wms_batch_item(job_id, row_no, action_type, raw_text, parsed_json, status, error_msg, warning_msg,
|
||||||
|
ref_id, area_name, product_name, brand, category, spec, unit, quantity,
|
||||||
|
operator_name, type_code, source_no, remark, create_time)
|
||||||
|
values(#{jobId}, #{rowNo}, #{actionType}, #{rawText}, #{parsedJson}, #{status}, #{errorMsg}, #{warningMsg},
|
||||||
|
#{refId}, #{areaName}, #{productName}, #{brand}, #{category}, #{spec}, #{unit}, #{quantity},
|
||||||
|
#{operatorName}, #{typeCode}, #{sourceNo}, #{remark}, sysdate())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="updateWmsBatchItem" parameterType="WmsBatchItem">
|
||||||
|
update wms_batch_item
|
||||||
|
<set>
|
||||||
|
row_no = #{rowNo},
|
||||||
|
action_type = #{actionType},
|
||||||
|
raw_text = #{rawText},
|
||||||
|
parsed_json = #{parsedJson},
|
||||||
|
status = #{status},
|
||||||
|
error_msg = #{errorMsg},
|
||||||
|
warning_msg = #{warningMsg},
|
||||||
|
ref_id = #{refId},
|
||||||
|
area_name = #{areaName},
|
||||||
|
product_name = #{productName},
|
||||||
|
brand = #{brand},
|
||||||
|
category = #{category},
|
||||||
|
spec = #{spec},
|
||||||
|
unit = #{unit},
|
||||||
|
quantity = #{quantity},
|
||||||
|
operator_name = #{operatorName},
|
||||||
|
type_code = #{typeCode},
|
||||||
|
source_no = #{sourceNo},
|
||||||
|
remark = #{remark},
|
||||||
|
update_time = sysdate()
|
||||||
|
</set>
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="deleteWmsBatchItemById" parameterType="Long">
|
||||||
|
delete from wms_batch_item where id = #{id}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<delete id="deleteWmsBatchItemByJobId" parameterType="Long">
|
||||||
|
delete from wms_batch_item where job_id = #{value}
|
||||||
|
</delete>
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
<?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.ruoyi.warehouse.mapper.WmsBatchJobMapper">
|
||||||
|
<resultMap type="WmsBatchJob" id="WmsBatchJobResult">
|
||||||
|
<id property="id" column="id"/>
|
||||||
|
<result property="jobNo" column="job_no"/>
|
||||||
|
<result property="sourceType" column="source_type"/>
|
||||||
|
<result property="bizType" column="biz_type"/>
|
||||||
|
<result property="fileName" column="file_name"/>
|
||||||
|
<result property="filePath" column="file_path"/>
|
||||||
|
<result property="status" column="status"/>
|
||||||
|
<result property="progress" column="progress"/>
|
||||||
|
<result property="totalCount" column="total_count"/>
|
||||||
|
<result property="successCount" column="success_count"/>
|
||||||
|
<result property="failCount" column="fail_count"/>
|
||||||
|
<result property="errorMsg" column="error_msg"/>
|
||||||
|
<result property="createBy" column="create_by"/>
|
||||||
|
<result property="createTime" column="create_time"/>
|
||||||
|
<result property="updateTime" column="update_time"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<sql id="selectWmsBatchJobVo">
|
||||||
|
select id, job_no, source_type, biz_type, file_name, file_path, status, progress,
|
||||||
|
total_count, success_count, fail_count, error_msg, create_by, create_time, update_time
|
||||||
|
from wms_batch_job
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="selectWmsBatchJobList" parameterType="WmsBatchJob" resultMap="WmsBatchJobResult">
|
||||||
|
<include refid="selectWmsBatchJobVo"/>
|
||||||
|
<where>
|
||||||
|
<if test="jobNo != null and jobNo != ''">and job_no like concat('%', #{jobNo}, '%')</if>
|
||||||
|
<if test="sourceType != null and sourceType != ''">and source_type = #{sourceType}</if>
|
||||||
|
<if test="bizType != null and bizType != ''">and biz_type = #{bizType}</if>
|
||||||
|
<if test="status != null">and status = #{status}</if>
|
||||||
|
</where>
|
||||||
|
order by id desc
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectWmsBatchJobById" parameterType="Long" resultMap="WmsBatchJobResult">
|
||||||
|
<include refid="selectWmsBatchJobVo"/> where id = #{id}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insertWmsBatchJob" parameterType="WmsBatchJob" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
insert into wms_batch_job(job_no, source_type, biz_type, file_name, file_path, status, progress,
|
||||||
|
total_count, success_count, fail_count, error_msg, create_by, create_time)
|
||||||
|
values(#{jobNo}, #{sourceType}, #{bizType}, #{fileName}, #{filePath}, #{status}, #{progress},
|
||||||
|
#{totalCount}, #{successCount}, #{failCount}, #{errorMsg}, #{createBy}, sysdate())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="updateWmsBatchJob" parameterType="WmsBatchJob">
|
||||||
|
update wms_batch_job
|
||||||
|
<set>
|
||||||
|
<if test="sourceType != null">source_type = #{sourceType},</if>
|
||||||
|
<if test="bizType != null">biz_type = #{bizType},</if>
|
||||||
|
<if test="fileName != null">file_name = #{fileName},</if>
|
||||||
|
<if test="filePath != null">file_path = #{filePath},</if>
|
||||||
|
<if test="status != null">status = #{status},</if>
|
||||||
|
<if test="progress != null">progress = #{progress},</if>
|
||||||
|
<if test="totalCount != null">total_count = #{totalCount},</if>
|
||||||
|
<if test="successCount != null">success_count = #{successCount},</if>
|
||||||
|
<if test="failCount != null">fail_count = #{failCount},</if>
|
||||||
|
error_msg = #{errorMsg},
|
||||||
|
update_time = sysdate()
|
||||||
|
</set>
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
</mapper>
|
||||||
46
ruoyi-ui/src/api/warehouse/batchImport.js
Normal file
46
ruoyi-ui/src/api/warehouse/batchImport.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function listBatchJob(query) {
|
||||||
|
return request({ url: '/warehouse/batch/list', method: 'get', params: query })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadBatchFile(file) {
|
||||||
|
const data = new FormData()
|
||||||
|
data.append('file', file)
|
||||||
|
return request({
|
||||||
|
url: '/warehouse/batch/upload',
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false },
|
||||||
|
timeout: 60000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBatchJob(id) {
|
||||||
|
return request({ url: '/warehouse/batch/' + id, method: 'get' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listBatchItems(jobId) {
|
||||||
|
return request({ url: '/warehouse/batch/' + jobId + '/items', method: 'get' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateBatchItem(data) {
|
||||||
|
return request({ url: '/warehouse/batch/item', method: 'put', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateBatchItemAction(jobId, data) {
|
||||||
|
return request({ url: '/warehouse/batch/' + jobId + '/items/action', method: 'put', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteBatchItem(id) {
|
||||||
|
return request({ url: '/warehouse/batch/item/' + id, method: 'delete' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function confirmBatchJob(jobId) {
|
||||||
|
return request({
|
||||||
|
url: '/warehouse/batch/' + jobId + '/confirm',
|
||||||
|
method: 'post',
|
||||||
|
timeout: 120000,
|
||||||
|
headers: { repeatSubmit: false }
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -67,6 +67,19 @@ export const constantRoutes = [
|
|||||||
hidden: true,
|
hidden: true,
|
||||||
meta: { title: '移动库存' }
|
meta: { title: '移动库存' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/warehouse/batch-import',
|
||||||
|
component: Layout,
|
||||||
|
hidden: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: () => import('@/views/warehouse/batchImport/index'),
|
||||||
|
name: 'WmsBatchImport',
|
||||||
|
meta: { title: '批量导入' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
|
|||||||
77
ruoyi-ui/src/utils/imageCompress.js
Normal file
77
ruoyi-ui/src/utils/imageCompress.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
function isImageFile(file) {
|
||||||
|
return file && file.type && file.type.indexOf('image/') === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function canvasToBlob(canvas, type, quality) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if (!canvas.toBlob) {
|
||||||
|
resolve(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
canvas.toBlob(blob => resolve(blob), type, quality)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadImage(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = event => {
|
||||||
|
const image = new Image()
|
||||||
|
image.onload = () => resolve(image)
|
||||||
|
image.onerror = reject
|
||||||
|
image.src = event.target.result
|
||||||
|
}
|
||||||
|
reader.onerror = reject
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function compressImageForUpload(file, options = {}) {
|
||||||
|
const maxSize = options.maxSize || 3 * 1024 * 1024
|
||||||
|
const maxWidth = options.maxWidth || 1600
|
||||||
|
const maxHeight = options.maxHeight || 1600
|
||||||
|
const outputType = options.outputType || 'image/jpeg'
|
||||||
|
const minQuality = options.minQuality || 0.55
|
||||||
|
|
||||||
|
if (!isImageFile(file) || typeof FileReader === 'undefined' || typeof document === 'undefined') {
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const image = await loadImage(file)
|
||||||
|
let scale = Math.min(maxWidth / image.width, maxHeight / image.height, 1)
|
||||||
|
let quality = options.quality || 0.82
|
||||||
|
let bestBlob = null
|
||||||
|
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
const width = Math.max(1, Math.round(image.width * scale))
|
||||||
|
const height = Math.max(1, Math.round(image.height * scale))
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = width
|
||||||
|
canvas.height = height
|
||||||
|
const context = canvas.getContext('2d')
|
||||||
|
context.fillStyle = '#ffffff'
|
||||||
|
context.fillRect(0, 0, width, height)
|
||||||
|
context.drawImage(image, 0, 0, width, height)
|
||||||
|
|
||||||
|
const blob = await canvasToBlob(canvas, outputType, quality)
|
||||||
|
if (!blob) {
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
bestBlob = blob
|
||||||
|
if (blob.size <= maxSize) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
scale = scale * 0.82
|
||||||
|
quality = Math.max(minQuality, quality - 0.08)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bestBlob || bestBlob.size >= file.size) {
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
const fileName = file.name ? file.name.replace(/\.[^.]+$/, '.jpg') : 'upload.jpg'
|
||||||
|
return new File([bestBlob], fileName, { type: outputType, lastModified: Date.now() })
|
||||||
|
} catch (error) {
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,7 +5,10 @@
|
|||||||
<h1>库存管理系统</h1>
|
<h1>库存管理系统</h1>
|
||||||
<p>面向库区、产品、入库、出库和盘库的日常库存作业台。</p>
|
<p>面向库区、产品、入库、出库和盘库的日常库存作业台。</p>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" icon="el-icon-mobile-phone" @click="$router.push('/mobile')">移动端入口</el-button>
|
<div class="hero-actions">
|
||||||
|
<el-button type="primary" icon="el-icon-upload2" @click="$router.push('/warehouse/batch-import')">批量导入</el-button>
|
||||||
|
<el-button type="primary" plain icon="el-icon-mobile-phone" @click="$router.push('/mobile')">移动端入口</el-button>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="agent-panel">
|
<section class="agent-panel">
|
||||||
@ -58,6 +61,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<el-table v-if="agent.plan.results && agent.plan.results.length" :data="agent.plan.results" size="mini" class="agent-table">
|
<el-table v-if="agent.plan.results && agent.plan.results.length" :data="agent.plan.results" size="mini" class="agent-table">
|
||||||
<el-table-column type="index" label="#" width="54" />
|
<el-table-column type="index" label="#" width="54" />
|
||||||
|
<el-table-column v-if="canSelectAgentResult(agent.plan)" label="选择" width="86">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button type="primary" size="mini" plain @click="selectAgentResult(scope.row)">选择</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="库区" prop="areaName" width="120" />
|
<el-table-column label="库区" prop="areaName" width="120" />
|
||||||
<el-table-column label="产品" min-width="220">
|
<el-table-column label="产品" min-width="220">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
@ -229,6 +237,60 @@ export default {
|
|||||||
this.agent.text = ''
|
this.agent.text = ''
|
||||||
this.agent.plan = null
|
this.agent.plan = null
|
||||||
},
|
},
|
||||||
|
canSelectAgentResult(plan) {
|
||||||
|
return plan && ['create_out_record', 'create_in_record', 'create_stock_check'].includes(plan.action) && plan.results && plan.results.length
|
||||||
|
},
|
||||||
|
selectAgentResult(item) {
|
||||||
|
if (!this.agent.plan || !item) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const plan = this.completePlanWithResult(this.agent.plan, item)
|
||||||
|
this.agent.plan = plan
|
||||||
|
this.agent.contextResults = [item]
|
||||||
|
this.$modal.msgSuccess('已选择:' + (item.productName || '库存项'))
|
||||||
|
},
|
||||||
|
completePlanWithResult(source, item) {
|
||||||
|
const plan = Object.assign({}, source, {
|
||||||
|
stockId: item.id,
|
||||||
|
areaId: item.areaId,
|
||||||
|
productId: item.productId,
|
||||||
|
areaName: item.areaName,
|
||||||
|
productName: item.productName,
|
||||||
|
brand: item.brand,
|
||||||
|
category: item.category,
|
||||||
|
spec: item.spec,
|
||||||
|
unit: item.unit,
|
||||||
|
currentStock: item.stockNum,
|
||||||
|
candidates: [],
|
||||||
|
results: [],
|
||||||
|
needConfirm: true,
|
||||||
|
message: '已选择产品,请确认后执行'
|
||||||
|
})
|
||||||
|
const quantity = this.toNumber(plan.quantity)
|
||||||
|
const stock = this.toNumber(item.stockNum)
|
||||||
|
if (plan.action === 'create_out_record') {
|
||||||
|
plan.executable = !!(plan.areaId && plan.productId && quantity > 0)
|
||||||
|
plan.afterStock = quantity > 0 && item.stockNum !== undefined && item.stockNum !== null ? stock - quantity : undefined
|
||||||
|
if (plan.executable && stock < quantity) {
|
||||||
|
plan.executable = false
|
||||||
|
plan.message = '库存不足,当前库存:' + item.stockNum
|
||||||
|
}
|
||||||
|
} else if (plan.action === 'create_in_record') {
|
||||||
|
plan.executable = !!(plan.areaId && plan.productId && quantity > 0)
|
||||||
|
plan.afterStock = quantity > 0 && item.stockNum !== undefined && item.stockNum !== null ? stock + quantity : undefined
|
||||||
|
} else if (plan.action === 'create_stock_check') {
|
||||||
|
plan.executable = !!(plan.areaId && plan.productId && quantity >= 0)
|
||||||
|
plan.afterStock = quantity >= 0 ? quantity : undefined
|
||||||
|
}
|
||||||
|
if (!plan.executable && plan.message === '已选择产品,请确认后执行') {
|
||||||
|
plan.message = '已选择产品,还缺少库区或数量'
|
||||||
|
}
|
||||||
|
return plan
|
||||||
|
},
|
||||||
|
toNumber(value) {
|
||||||
|
const numberValue = Number(value)
|
||||||
|
return Number.isFinite(numberValue) ? numberValue : 0
|
||||||
|
},
|
||||||
actionLabel(action) {
|
actionLabel(action) {
|
||||||
const map = {
|
const map = {
|
||||||
query_stock: '查询库存',
|
query_stock: '查询库存',
|
||||||
@ -278,6 +340,12 @@ export default {
|
|||||||
border: 1px solid #e6ebf2;
|
border: 1px solid #e6ebf2;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
.hero h1 {
|
.hero h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
|||||||
@ -76,12 +76,15 @@
|
|||||||
<div v-if="agent.plan.candidates && agent.plan.candidates.length" class="agent-candidates">
|
<div v-if="agent.plan.candidates && agent.plan.candidates.length" class="agent-candidates">
|
||||||
<span v-for="item in agent.plan.candidates" :key="item.type + item.id">{{ item.name }} {{ item.detail || '' }}</span>
|
<span v-for="item in agent.plan.candidates" :key="item.type + item.id">{{ item.name }} {{ item.detail || '' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<article v-for="(item, index) in (agent.plan.results || [])" :key="'agent-result-' + item.id" class="agent-stock">
|
<article v-for="(item, index) in (agent.plan.results || [])" :key="'agent-result-' + index + '-' + (item.id || item.productId || '')" class="agent-stock">
|
||||||
<div>
|
<div>
|
||||||
<div class="card-title">第 {{ index + 1 }} 项:{{ item.productName || '-' }}</div>
|
<div class="card-title">第 {{ index + 1 }} 项:{{ item.productName || '-' }}</div>
|
||||||
<div class="meta-line">{{ item.areaName || '-' }} | {{ productMeta(item) }}</div>
|
<div class="meta-line">{{ item.areaName || '-' }} | {{ productMeta(item) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<strong>{{ item.stockNum }}</strong>
|
<div class="agent-stock-action">
|
||||||
|
<strong>{{ item.stockNum === undefined || item.stockNum === null ? '-' : item.stockNum }}</strong>
|
||||||
|
<el-button v-if="canSelectAgentResult(agent.plan)" type="primary" size="mini" plain @click="selectAgentResult(item)">选择</el-button>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -93,6 +96,7 @@
|
|||||||
<button @click="switchTab('stock')">查库存</button>
|
<button @click="switchTab('stock')">查库存</button>
|
||||||
<button @click="switchTab('product')" v-hasPermi="['warehouse:product:list']">产品维护</button>
|
<button @click="switchTab('product')" v-hasPermi="['warehouse:product:list']">产品维护</button>
|
||||||
<button @click="switchTab('area')" v-hasPermi="['warehouse:area:list']">库区维护</button>
|
<button @click="switchTab('area')" v-hasPermi="['warehouse:area:list']">库区维护</button>
|
||||||
|
<button @click="switchTab('batch')">批量导入</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-title">库存概览</div>
|
<div class="panel-title">库存概览</div>
|
||||||
@ -300,6 +304,108 @@
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section v-show="activeTab === 'batch'" class="mobile-section">
|
||||||
|
<div class="section-head">
|
||||||
|
<div>
|
||||||
|
<div class="section-title">智能批量导入</div>
|
||||||
|
<div class="section-subtitle">上传 Excel 或图片,解析后确认再写入库存数据</div>
|
||||||
|
</div>
|
||||||
|
<el-button size="mini" icon="el-icon-arrow-left" @click="switchTab('home')">返回</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-upload
|
||||||
|
class="mobile-upload"
|
||||||
|
action="#"
|
||||||
|
accept=".xls,.xlsx,.jpg,.jpeg,.png,.bmp"
|
||||||
|
:show-file-list="false"
|
||||||
|
:http-request="handleBatchUpload"
|
||||||
|
:disabled="batch.uploading"
|
||||||
|
>
|
||||||
|
<el-button class="full-button" type="primary" icon="el-icon-upload2" :loading="batch.uploading">上传 Excel / 图片</el-button>
|
||||||
|
<div slot="tip" class="upload-tip">图片识别耗时更久,上传后请等待解析进度完成。</div>
|
||||||
|
</el-upload>
|
||||||
|
|
||||||
|
<div v-if="batch.currentJob" class="batch-current">
|
||||||
|
<div class="card-head">
|
||||||
|
<div>
|
||||||
|
<div class="card-title">{{ batch.currentJob.jobNo }}</div>
|
||||||
|
<div class="card-subtitle">{{ batch.currentJob.fileName }} · {{ batchStatusLabel(batch.currentJob.status) }}</div>
|
||||||
|
</div>
|
||||||
|
<el-tag size="mini" :type="batchStatusType(batch.currentJob.status)">{{ batchSourceLabel(batch.currentJob.sourceType) }}</el-tag>
|
||||||
|
</div>
|
||||||
|
<el-progress :percentage="batch.currentJob.progress || 0" :status="batchProgressStatus(batch.currentJob)" />
|
||||||
|
<div class="batch-stats">
|
||||||
|
<el-tag size="mini">总数 {{ batch.currentJob.totalCount || 0 }}</el-tag>
|
||||||
|
<el-tag size="mini" type="success">可执行 {{ batch.currentJob.successCount || 0 }}</el-tag>
|
||||||
|
<el-tag size="mini" type="danger">异常 {{ batch.currentJob.failCount || 0 }}</el-tag>
|
||||||
|
</div>
|
||||||
|
<el-alert v-if="batch.currentJob.errorMsg" :title="batch.currentJob.errorMsg" type="warning" show-icon :closable="false" />
|
||||||
|
<el-button
|
||||||
|
class="full-button confirm-button"
|
||||||
|
type="danger"
|
||||||
|
icon="el-icon-check"
|
||||||
|
:loading="batch.confirming"
|
||||||
|
:disabled="batch.currentJob.status !== 2 || batch.currentJob.failCount > 0 || !batch.items.length"
|
||||||
|
@click="confirmBatch"
|
||||||
|
>确认执行当前任务</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="batch.items.length" class="batch-action-panel">
|
||||||
|
<el-select v-model="batch.actionType" placeholder="批量设置操作类型" clearable>
|
||||||
|
<el-option label="新增库区" value="create_area" />
|
||||||
|
<el-option label="新增产品" value="create_product" />
|
||||||
|
<el-option label="入库登记" value="create_in_record" />
|
||||||
|
<el-option label="出库" value="create_out_record" />
|
||||||
|
<el-option label="盘库登记" value="create_stock_check" />
|
||||||
|
</el-select>
|
||||||
|
<div class="batch-action-buttons">
|
||||||
|
<el-button type="primary" plain :loading="batch.batchSaving" :disabled="!canEditBatchItems || !batch.actionType" @click="applyMobileBatchAction(false)">应用到全部</el-button>
|
||||||
|
<el-button type="primary" plain :loading="batch.batchSaving" :disabled="!canEditBatchItems || !batch.actionType || !batch.selectedItemIds.length" @click="applyMobileBatchAction(true)">应用已选 {{ batch.selectedItemIds.length }}</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="batch.itemLoading" class="empty-tip">正在加载明细...</div>
|
||||||
|
<div v-else-if="batch.currentJob && batch.items.length === 0" class="empty-tip">暂无解析明细</div>
|
||||||
|
<article v-for="item in batch.items" :key="'batch-item-' + item.id" class="mobile-card batch-item-card">
|
||||||
|
<div class="card-head">
|
||||||
|
<div>
|
||||||
|
<div class="card-title">第 {{ item.rowNo }} 行 · {{ batchActionLabel(item.actionType) }}</div>
|
||||||
|
<div class="card-subtitle">{{ item.areaName || '未识别库区' }}</div>
|
||||||
|
</div>
|
||||||
|
<el-checkbox :value="isBatchItemSelected(item)" :disabled="!canEditBatchItems" @change="toggleBatchItem(item, $event)">选择</el-checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="batch-product">{{ item.productName || '未识别产品' }}</div>
|
||||||
|
<div class="meta-line">{{ batchProductMeta(item) || '无产品明细' }}</div>
|
||||||
|
<div class="record-line">
|
||||||
|
<span v-if="item.quantity !== undefined && item.quantity !== null">数量:{{ item.quantity }}</span>
|
||||||
|
<span v-if="item.operatorName">人员:{{ item.operatorName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="batch-row-status">
|
||||||
|
<el-tag size="mini" :type="batchItemStatusType(item.status)">{{ batchItemStatusLabel(item.status) }}</el-tag>
|
||||||
|
<span v-if="item.errorMsg" class="error-text">{{ item.errorMsg }}</span>
|
||||||
|
<span v-else-if="item.warningMsg">{{ item.warningMsg }}</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div class="panel-title">最近任务</div>
|
||||||
|
<div v-if="batch.jobLoading" class="empty-tip">正在加载任务...</div>
|
||||||
|
<div v-else-if="batch.jobList.length === 0" class="empty-tip">暂无批量任务</div>
|
||||||
|
<article v-for="job in batch.jobList" :key="'batch-job-' + job.id" class="mobile-card batch-job-card" @click="selectBatchJob(job)">
|
||||||
|
<div class="card-head">
|
||||||
|
<div>
|
||||||
|
<div class="card-title">{{ job.jobNo }}</div>
|
||||||
|
<div class="card-subtitle">{{ job.fileName || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<el-tag size="mini" :type="batchStatusType(job.status)">{{ batchStatusLabel(job.status) }}</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="record-line">
|
||||||
|
<span>{{ batchSourceLabel(job.sourceType) }}</span>
|
||||||
|
<span>进度 {{ job.progress || 0 }}%</span>
|
||||||
|
<span>异常 {{ job.failCount || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<nav class="bottom-nav">
|
<nav class="bottom-nav">
|
||||||
@ -435,6 +541,8 @@ import { listInRecord, getInRecord, addInRecord, updateInRecord, delInRecord, co
|
|||||||
import { listOutRecord, addOutRecord } from '@/api/warehouse/outRecord'
|
import { listOutRecord, addOutRecord } from '@/api/warehouse/outRecord'
|
||||||
import { listStockCheck, addStockCheck, confirmStockCheck } from '@/api/warehouse/stockCheck'
|
import { listStockCheck, addStockCheck, confirmStockCheck } from '@/api/warehouse/stockCheck'
|
||||||
import { analyzeAgent, executeAgent } from '@/api/warehouse/agent'
|
import { analyzeAgent, executeAgent } from '@/api/warehouse/agent'
|
||||||
|
import { listBatchJob, uploadBatchFile, getBatchJob, listBatchItems, updateBatchItemAction, confirmBatchJob } from '@/api/warehouse/batchImport'
|
||||||
|
import { compressImageForUpload } from '@/utils/imageCompress'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MobileWarehouse',
|
name: 'MobileWarehouse',
|
||||||
@ -470,6 +578,19 @@ export default {
|
|||||||
contextResults: [],
|
contextResults: [],
|
||||||
pendingNextPlan: null,
|
pendingNextPlan: null,
|
||||||
plan: null
|
plan: null
|
||||||
|
},
|
||||||
|
batch: {
|
||||||
|
uploading: false,
|
||||||
|
confirming: false,
|
||||||
|
jobLoading: false,
|
||||||
|
itemLoading: false,
|
||||||
|
batchSaving: false,
|
||||||
|
actionType: undefined,
|
||||||
|
jobList: [],
|
||||||
|
currentJob: null,
|
||||||
|
items: [],
|
||||||
|
selectedItemIds: [],
|
||||||
|
pollTimer: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -484,6 +605,9 @@ export default {
|
|||||||
unitOptions() {
|
unitOptions() {
|
||||||
return this.distinctProductOptions('unit')
|
return this.distinctProductOptions('unit')
|
||||||
},
|
},
|
||||||
|
canEditBatchItems() {
|
||||||
|
return this.batch.currentJob && ![3, 4].includes(this.batch.currentJob.status)
|
||||||
|
},
|
||||||
agentPlanItems() {
|
agentPlanItems() {
|
||||||
const plan = this.agent.plan
|
const plan = this.agent.plan
|
||||||
if (!plan) {
|
if (!plan) {
|
||||||
@ -508,6 +632,9 @@ export default {
|
|||||||
this.loadOptions()
|
this.loadOptions()
|
||||||
this.refreshAll()
|
this.refreshAll()
|
||||||
},
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.stopBatchPolling()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
analyzeAgentText() {
|
analyzeAgentText() {
|
||||||
const text = (this.agent.text || '').trim()
|
const text = (this.agent.text || '').trim()
|
||||||
@ -546,6 +673,64 @@ export default {
|
|||||||
this.agent.plan = null
|
this.agent.plan = null
|
||||||
this.agent.pendingNextPlan = null
|
this.agent.pendingNextPlan = null
|
||||||
},
|
},
|
||||||
|
canSelectAgentResult(plan) {
|
||||||
|
return plan && ['create_out_record', 'create_in_record', 'create_stock_check'].includes(plan.action) && plan.results && plan.results.length
|
||||||
|
},
|
||||||
|
selectAgentResult(item) {
|
||||||
|
if (!this.agent.plan || !item) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const plan = this.completePlanWithResult(this.agent.plan, item)
|
||||||
|
this.agent.plan = plan
|
||||||
|
this.agent.contextResults = [item]
|
||||||
|
if (plan.executable) {
|
||||||
|
this.autoFillAgentForm(plan)
|
||||||
|
} else {
|
||||||
|
this.$modal.msgWarning(plan.message || '已选择产品,请补充缺少的信息')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
completePlanWithResult(source, item) {
|
||||||
|
const plan = Object.assign({}, source, {
|
||||||
|
stockId: item.id,
|
||||||
|
areaId: item.areaId,
|
||||||
|
productId: item.productId,
|
||||||
|
areaName: item.areaName,
|
||||||
|
productName: item.productName,
|
||||||
|
brand: item.brand,
|
||||||
|
category: item.category,
|
||||||
|
spec: item.spec,
|
||||||
|
unit: item.unit,
|
||||||
|
currentStock: item.stockNum,
|
||||||
|
candidates: [],
|
||||||
|
results: [],
|
||||||
|
needConfirm: true,
|
||||||
|
message: '已选择产品,请确认后执行'
|
||||||
|
})
|
||||||
|
const quantity = this.toNumber(plan.quantity)
|
||||||
|
const stock = this.toNumber(item.stockNum)
|
||||||
|
if (plan.action === 'create_out_record') {
|
||||||
|
plan.executable = !!(plan.areaId && plan.productId && quantity > 0)
|
||||||
|
plan.afterStock = quantity > 0 && item.stockNum !== undefined && item.stockNum !== null ? stock - quantity : undefined
|
||||||
|
if (plan.executable && stock < quantity) {
|
||||||
|
plan.executable = false
|
||||||
|
plan.message = '库存不足,当前库存:' + item.stockNum
|
||||||
|
}
|
||||||
|
} else if (plan.action === 'create_in_record') {
|
||||||
|
plan.executable = !!(plan.areaId && plan.productId && quantity > 0)
|
||||||
|
plan.afterStock = quantity > 0 && item.stockNum !== undefined && item.stockNum !== null ? stock + quantity : undefined
|
||||||
|
} else if (plan.action === 'create_stock_check') {
|
||||||
|
plan.executable = !!(plan.areaId && plan.productId && quantity >= 0)
|
||||||
|
plan.afterStock = quantity >= 0 ? quantity : undefined
|
||||||
|
}
|
||||||
|
if (!plan.executable && plan.message === '已选择产品,请确认后执行') {
|
||||||
|
plan.message = '已选择产品,还缺少库区或数量'
|
||||||
|
}
|
||||||
|
return plan
|
||||||
|
},
|
||||||
|
toNumber(value) {
|
||||||
|
const numberValue = Number(value)
|
||||||
|
return Number.isFinite(numberValue) ? numberValue : 0
|
||||||
|
},
|
||||||
autoFillAgentForm(plan) {
|
autoFillAgentForm(plan) {
|
||||||
if (!plan || !plan.executable) {
|
if (!plan || !plan.executable) {
|
||||||
return
|
return
|
||||||
@ -561,7 +746,7 @@ export default {
|
|||||||
spec: plan.spec,
|
spec: plan.spec,
|
||||||
unit: plan.unit,
|
unit: plan.unit,
|
||||||
outOperator: plan.operatorName,
|
outOperator: plan.operatorName,
|
||||||
outType: plan.outType || 3,
|
outType: plan.outType || 1,
|
||||||
outNum: plan.quantity,
|
outNum: plan.quantity,
|
||||||
sourceNo: undefined,
|
sourceNo: undefined,
|
||||||
remark: plan.remark
|
remark: plan.remark
|
||||||
@ -574,7 +759,7 @@ export default {
|
|||||||
areaId: plan.areaId,
|
areaId: plan.areaId,
|
||||||
productId: plan.productId,
|
productId: plan.productId,
|
||||||
outOperator: plan.operatorName,
|
outOperator: plan.operatorName,
|
||||||
outType: plan.outType || 3,
|
outType: plan.outType || 1,
|
||||||
outNum: plan.quantity,
|
outNum: plan.quantity,
|
||||||
sourceNo: undefined,
|
sourceNo: undefined,
|
||||||
remark: plan.remark
|
remark: plan.remark
|
||||||
@ -697,6 +882,156 @@ export default {
|
|||||||
loadAreas() {
|
loadAreas() {
|
||||||
listArea(this.areaQuery).then(res => { this.areaList = res.rows || [] })
|
listArea(this.areaQuery).then(res => { this.areaList = res.rows || [] })
|
||||||
},
|
},
|
||||||
|
async handleBatchUpload(option) {
|
||||||
|
this.batch.uploading = true
|
||||||
|
const uploadFile = await compressImageForUpload(option.file)
|
||||||
|
if (uploadFile !== option.file) {
|
||||||
|
this.$modal.msgSuccess('照片已压缩,正在上传解析')
|
||||||
|
}
|
||||||
|
uploadBatchFile(uploadFile).then(res => {
|
||||||
|
this.batch.currentJob = res.data
|
||||||
|
this.batch.items = []
|
||||||
|
this.batch.selectedItemIds = []
|
||||||
|
this.$modal.msgSuccess('上传成功,正在后台解析')
|
||||||
|
this.startBatchPolling(res.data.id)
|
||||||
|
this.loadBatchJobs()
|
||||||
|
}).finally(() => {
|
||||||
|
this.batch.uploading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
loadBatchJobs() {
|
||||||
|
this.batch.jobLoading = true
|
||||||
|
listBatchJob({ pageNum: 1, pageSize: 10 }).then(res => {
|
||||||
|
this.batch.jobList = res.rows || []
|
||||||
|
}).finally(() => {
|
||||||
|
this.batch.jobLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
selectBatchJob(row) {
|
||||||
|
this.batch.currentJob = row
|
||||||
|
this.loadBatchItems(row.id)
|
||||||
|
if ([0, 1, 3].includes(row.status)) {
|
||||||
|
this.startBatchPolling(row.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startBatchPolling(jobId) {
|
||||||
|
this.stopBatchPolling()
|
||||||
|
this.batch.pollTimer = setInterval(() => {
|
||||||
|
this.refreshBatchJob(jobId)
|
||||||
|
}, 1500)
|
||||||
|
this.refreshBatchJob(jobId)
|
||||||
|
},
|
||||||
|
stopBatchPolling() {
|
||||||
|
if (this.batch.pollTimer) {
|
||||||
|
clearInterval(this.batch.pollTimer)
|
||||||
|
this.batch.pollTimer = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refreshBatchJob(jobId) {
|
||||||
|
getBatchJob(jobId).then(res => {
|
||||||
|
this.batch.currentJob = res.data
|
||||||
|
if (this.batch.currentJob && [2, 4, 5, 6].includes(this.batch.currentJob.status)) {
|
||||||
|
this.stopBatchPolling()
|
||||||
|
this.loadBatchItems(jobId)
|
||||||
|
this.loadBatchJobs()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
loadBatchItems(jobId) {
|
||||||
|
this.batch.itemLoading = true
|
||||||
|
listBatchItems(jobId).then(res => {
|
||||||
|
this.batch.items = res.data || []
|
||||||
|
this.batch.selectedItemIds = []
|
||||||
|
}).finally(() => {
|
||||||
|
this.batch.itemLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
isBatchItemSelected(item) {
|
||||||
|
return this.batch.selectedItemIds.includes(item.id)
|
||||||
|
},
|
||||||
|
toggleBatchItem(item, checked) {
|
||||||
|
const ids = this.batch.selectedItemIds.slice()
|
||||||
|
const index = ids.indexOf(item.id)
|
||||||
|
if (checked && index === -1) {
|
||||||
|
ids.push(item.id)
|
||||||
|
}
|
||||||
|
if (!checked && index !== -1) {
|
||||||
|
ids.splice(index, 1)
|
||||||
|
}
|
||||||
|
this.batch.selectedItemIds = ids
|
||||||
|
},
|
||||||
|
applyMobileBatchAction(onlySelected) {
|
||||||
|
if (!this.batch.currentJob || !this.batch.actionType) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const itemIds = onlySelected ? this.batch.selectedItemIds : undefined
|
||||||
|
if (onlySelected && !itemIds.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const label = this.batchActionLabel(this.batch.actionType)
|
||||||
|
this.$modal.confirm('确认将' + (onlySelected ? '已选' : '全部') + '明细的操作类型设置为“' + label + '”?').then(() => {
|
||||||
|
this.batch.batchSaving = true
|
||||||
|
return updateBatchItemAction(this.batch.currentJob.id, { actionType: this.batch.actionType, itemIds })
|
||||||
|
}).then(() => {
|
||||||
|
this.$modal.msgSuccess('批量设置完成')
|
||||||
|
this.loadBatchItems(this.batch.currentJob.id)
|
||||||
|
this.refreshBatchJob(this.batch.currentJob.id)
|
||||||
|
}).finally(() => {
|
||||||
|
this.batch.batchSaving = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
confirmBatch() {
|
||||||
|
if (!this.batch.currentJob) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$modal.confirm('确认执行当前批量任务?执行后会真正写入库存业务数据。').then(() => {
|
||||||
|
this.batch.confirming = true
|
||||||
|
return confirmBatchJob(this.batch.currentJob.id)
|
||||||
|
}).then(res => {
|
||||||
|
this.batch.currentJob = res.data
|
||||||
|
this.$modal.msgSuccess('执行完成')
|
||||||
|
this.loadBatchItems(this.batch.currentJob.id)
|
||||||
|
this.loadBatchJobs()
|
||||||
|
this.refreshAll()
|
||||||
|
this.loadOptions()
|
||||||
|
}).finally(() => {
|
||||||
|
this.batch.confirming = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
batchStatusLabel(status) {
|
||||||
|
return { 0: '已上传', 1: '解析中', 2: '待确认', 3: '执行中', 4: '完成', 5: '失败', 6: '已取消' }[status] || '-'
|
||||||
|
},
|
||||||
|
batchStatusType(status) {
|
||||||
|
return { 2: 'warning', 4: 'success', 5: 'danger', 6: 'info' }[status] || 'info'
|
||||||
|
},
|
||||||
|
batchSourceLabel(sourceType) {
|
||||||
|
return { excel: 'Excel', image: '图片', file: '文件' }[sourceType] || sourceType || '-'
|
||||||
|
},
|
||||||
|
batchItemStatusLabel(status) {
|
||||||
|
return { 1: '可执行', 2: '异常', 3: '已执行', 4: '失败' }[status] || '-'
|
||||||
|
},
|
||||||
|
batchItemStatusType(status) {
|
||||||
|
return { 1: 'success', 2: 'danger', 3: 'success', 4: 'danger' }[status] || 'info'
|
||||||
|
},
|
||||||
|
batchProgressStatus(job) {
|
||||||
|
if (!job) return undefined
|
||||||
|
if (job.status === 4) return 'success'
|
||||||
|
if (job.status === 5) return 'exception'
|
||||||
|
return undefined
|
||||||
|
},
|
||||||
|
batchActionLabel(action) {
|
||||||
|
return {
|
||||||
|
create_area: '新增库区',
|
||||||
|
create_product: '新增产品',
|
||||||
|
create_in_record: '入库登记',
|
||||||
|
create_out_record: '出库',
|
||||||
|
create_stock_check: '盘库登记',
|
||||||
|
unknown: '未识别'
|
||||||
|
}[action] || action || '-'
|
||||||
|
},
|
||||||
|
batchProductMeta(row) {
|
||||||
|
return [row.brand, row.category, row.spec, row.unit].filter(Boolean).join(' / ')
|
||||||
|
},
|
||||||
switchTab(tab) {
|
switchTab(tab) {
|
||||||
this.activeTab = tab
|
this.activeTab = tab
|
||||||
if (tab === 'stock') this.loadStock()
|
if (tab === 'stock') this.loadStock()
|
||||||
@ -705,6 +1040,7 @@ export default {
|
|||||||
if (tab === 'check') this.loadCheckRecords()
|
if (tab === 'check') this.loadCheckRecords()
|
||||||
if (tab === 'product') this.loadProducts()
|
if (tab === 'product') this.loadProducts()
|
||||||
if (tab === 'area') this.loadAreas()
|
if (tab === 'area') this.loadAreas()
|
||||||
|
if (tab === 'batch') this.loadBatchJobs()
|
||||||
if (tab === 'home') this.refreshAll()
|
if (tab === 'home') this.refreshAll()
|
||||||
},
|
},
|
||||||
areaLabel(area) {
|
areaLabel(area) {
|
||||||
@ -739,7 +1075,7 @@ export default {
|
|||||||
areaId: undefined,
|
areaId: undefined,
|
||||||
productId: undefined,
|
productId: undefined,
|
||||||
inOperator: undefined,
|
inOperator: undefined,
|
||||||
inType: 1,
|
inType: 2,
|
||||||
inNum: undefined,
|
inNum: undefined,
|
||||||
remark: undefined
|
remark: undefined
|
||||||
}, data || {})
|
}, data || {})
|
||||||
@ -1133,6 +1469,13 @@ export default {
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.agent-stock-action {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
.quick-panel {
|
.quick-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
@ -1181,6 +1524,70 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
.mobile-upload {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.mobile-upload ::v-deep .el-upload {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.upload-tip {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
.batch-current,
|
||||||
|
.batch-action-panel {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e8edf3;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.batch-current .el-progress {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.batch-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.confirm-button {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.batch-action-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.batch-action-buttons {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.batch-action-buttons .el-button {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
.batch-product {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
.batch-row-status {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #7a8794;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
.batch-job-card {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.error-text {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
.mobile-card {
|
.mobile-card {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid #e8edf3;
|
border: 1px solid #e8edf3;
|
||||||
|
|||||||
415
ruoyi-ui/src/views/warehouse/batchImport/index.vue
Normal file
415
ruoyi-ui/src/views/warehouse/batchImport/index.vue
Normal file
@ -0,0 +1,415 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container batch-page">
|
||||||
|
<el-card shadow="never" class="batch-card">
|
||||||
|
<div slot="header" class="card-head">
|
||||||
|
<div>
|
||||||
|
<span class="title">智能批量导入</span>
|
||||||
|
<span class="subtitle">支持 Excel 和图片,解析后先确认明细,再写入库存业务表。</span>
|
||||||
|
</div>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="loadJobs">刷新任务</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-upload
|
||||||
|
class="upload-box"
|
||||||
|
drag
|
||||||
|
action="#"
|
||||||
|
accept=".xls,.xlsx,.jpg,.jpeg,.png,.bmp"
|
||||||
|
:show-file-list="false"
|
||||||
|
:http-request="handleUpload"
|
||||||
|
:disabled="uploading"
|
||||||
|
>
|
||||||
|
<i class="el-icon-upload"></i>
|
||||||
|
<div class="el-upload__text">拖入 Excel 或图片,或 <em>点击上传</em></div>
|
||||||
|
<div slot="tip" class="el-upload__tip">Excel 支持 xls/xlsx;图片支持 jpg/png/bmp。图片会调用大模型识别,耗时可能更久。</div>
|
||||||
|
</el-upload>
|
||||||
|
|
||||||
|
<div v-if="currentJob" class="job-panel">
|
||||||
|
<div class="job-main">
|
||||||
|
<div>
|
||||||
|
<div class="job-no">{{ currentJob.jobNo }}</div>
|
||||||
|
<div class="job-meta">{{ currentJob.fileName }} | {{ statusLabel(currentJob.status) }}</div>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
icon="el-icon-check"
|
||||||
|
:loading="confirming"
|
||||||
|
:disabled="currentJob.status !== 2 || currentJob.failCount > 0 || !items.length"
|
||||||
|
@click="handleConfirm"
|
||||||
|
>确认执行</el-button>
|
||||||
|
</div>
|
||||||
|
<el-progress :percentage="currentJob.progress || 0" :status="progressStatus(currentJob)" />
|
||||||
|
<div class="job-stats">
|
||||||
|
<el-tag size="small">总数 {{ currentJob.totalCount || 0 }}</el-tag>
|
||||||
|
<el-tag size="small" type="success">可执行 {{ currentJob.successCount || 0 }}</el-tag>
|
||||||
|
<el-tag size="small" type="danger">异常 {{ currentJob.failCount || 0 }}</el-tag>
|
||||||
|
<el-alert v-if="currentJob.errorMsg" :title="currentJob.errorMsg" type="warning" show-icon :closable="false" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :xs="24" :lg="7">
|
||||||
|
<el-card shadow="never" class="batch-card">
|
||||||
|
<div slot="header" class="card-head">
|
||||||
|
<span class="title">最近任务</span>
|
||||||
|
</div>
|
||||||
|
<el-table v-loading="jobLoading" :data="jobList" size="mini" height="420" @row-click="selectJob">
|
||||||
|
<el-table-column label="任务号" prop="jobNo" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column label="来源" width="72">
|
||||||
|
<template slot-scope="scope">{{ sourceLabel(scope.row.sourceType) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="86">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="statusType(scope.row.status)" size="mini">{{ statusLabel(scope.row.status) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="进度" prop="progress" width="70" align="right" />
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xs="24" :lg="17">
|
||||||
|
<el-card shadow="never" class="batch-card">
|
||||||
|
<div slot="header" class="card-head">
|
||||||
|
<span class="title">待确认明细</span>
|
||||||
|
<span class="subtitle">异常明细需要修改或删除后才能确认执行。</span>
|
||||||
|
</div>
|
||||||
|
<div class="batch-actions">
|
||||||
|
<el-select v-model="batchActionType" size="small" placeholder="批量设置操作类型" clearable>
|
||||||
|
<el-option label="新增库区" value="create_area" />
|
||||||
|
<el-option label="新增产品" value="create_product" />
|
||||||
|
<el-option label="入库登记" value="create_in_record" />
|
||||||
|
<el-option label="出库" value="create_out_record" />
|
||||||
|
<el-option label="盘库登记" value="create_stock_check" />
|
||||||
|
</el-select>
|
||||||
|
<el-button size="small" type="primary" plain :loading="batchSaving" :disabled="!canEditItems || !batchActionType || !items.length" @click="applyBatchAction(false)">应用到全部</el-button>
|
||||||
|
<el-button size="small" type="primary" plain :loading="batchSaving" :disabled="!canEditItems || !batchActionType || !selectedItems.length" @click="applyBatchAction(true)">应用到已选 {{ selectedItems.length }}</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table v-loading="itemLoading" :data="items" size="mini" height="520" @selection-change="handleItemSelectionChange">
|
||||||
|
<el-table-column type="selection" width="45" />
|
||||||
|
<el-table-column label="行" prop="rowNo" width="58" />
|
||||||
|
<el-table-column label="操作" width="110">
|
||||||
|
<template slot-scope="scope">{{ actionLabel(scope.row.actionType) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="92">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="itemStatusType(scope.row.status)" size="mini">{{ itemStatusLabel(scope.row.status) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="库区" prop="areaName" width="110" show-overflow-tooltip />
|
||||||
|
<el-table-column label="产品" min-width="190" show-overflow-tooltip>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<div>{{ scope.row.productName || '-' }}</div>
|
||||||
|
<div class="meta">{{ productMeta(scope.row) }}</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="数量" prop="quantity" width="90" align="right" />
|
||||||
|
<el-table-column label="人员" prop="operatorName" width="100" show-overflow-tooltip />
|
||||||
|
<el-table-column label="提示/错误" min-width="220" show-overflow-tooltip>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span class="error-text" v-if="scope.row.errorMsg">{{ scope.row.errorMsg }}</span>
|
||||||
|
<span v-else>{{ scope.row.warningMsg || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="120" fixed="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button type="text" size="mini" @click="openEdit(scope.row)">修改</el-button>
|
||||||
|
<el-button type="text" size="mini" class="danger-link" @click="removeItem(scope.row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-dialog title="修改明细" :visible.sync="editOpen" width="620px" append-to-body>
|
||||||
|
<el-form ref="editForm" :model="editForm" label-width="90px">
|
||||||
|
<el-form-item label="操作类型">
|
||||||
|
<el-select v-model="editForm.actionType" placeholder="请选择操作类型" style="width: 100%">
|
||||||
|
<el-option label="新增库区" value="create_area" />
|
||||||
|
<el-option label="新增产品" value="create_product" />
|
||||||
|
<el-option label="入库登记" value="create_in_record" />
|
||||||
|
<el-option label="出库" value="create_out_record" />
|
||||||
|
<el-option label="盘库登记" value="create_stock_check" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="库区"><el-input v-model="editForm.areaName" /></el-form-item>
|
||||||
|
<el-form-item label="产品名称"><el-input v-model="editForm.productName" /></el-form-item>
|
||||||
|
<el-row :gutter="12">
|
||||||
|
<el-col :span="12"><el-form-item label="品牌"><el-input v-model="editForm.brand" /></el-form-item></el-col>
|
||||||
|
<el-col :span="12"><el-form-item label="类别"><el-input v-model="editForm.category" /></el-form-item></el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="12">
|
||||||
|
<el-col :span="12"><el-form-item label="规格"><el-input v-model="editForm.spec" /></el-form-item></el-col>
|
||||||
|
<el-col :span="12"><el-form-item label="单位"><el-input v-model="editForm.unit" /></el-form-item></el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="12">
|
||||||
|
<el-col :span="12"><el-form-item label="数量"><el-input-number v-model="editForm.quantity" :precision="3" :min="0" style="width: 100%" /></el-form-item></el-col>
|
||||||
|
<el-col :span="12"><el-form-item label="类型编码"><el-input-number v-model="editForm.typeCode" :min="1" :max="4" style="width: 100%" /></el-form-item></el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item label="操作人"><el-input v-model="editForm.operatorName" /></el-form-item>
|
||||||
|
<el-form-item label="备注"><el-input v-model="editForm.remark" type="textarea" /></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" :loading="savingItem" @click="saveItem">保 存</el-button>
|
||||||
|
<el-button @click="editOpen = false">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listBatchJob, uploadBatchFile, getBatchJob, listBatchItems, updateBatchItem, updateBatchItemAction, deleteBatchItem, confirmBatchJob } from '@/api/warehouse/batchImport'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'WmsBatchImport',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
uploading: false,
|
||||||
|
confirming: false,
|
||||||
|
jobLoading: false,
|
||||||
|
itemLoading: false,
|
||||||
|
savingItem: false,
|
||||||
|
batchSaving: false,
|
||||||
|
batchActionType: undefined,
|
||||||
|
jobList: [],
|
||||||
|
currentJob: null,
|
||||||
|
items: [],
|
||||||
|
selectedItems: [],
|
||||||
|
pollTimer: null,
|
||||||
|
editOpen: false,
|
||||||
|
editForm: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadJobs()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.stopPolling()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleUpload(option) {
|
||||||
|
this.uploading = true
|
||||||
|
uploadBatchFile(option.file).then(res => {
|
||||||
|
this.currentJob = res.data
|
||||||
|
this.items = []
|
||||||
|
this.$modal.msgSuccess('上传成功,正在后台解析,请留意进度')
|
||||||
|
this.startPolling(res.data.id)
|
||||||
|
this.loadJobs()
|
||||||
|
}).finally(() => {
|
||||||
|
this.uploading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
loadJobs() {
|
||||||
|
this.jobLoading = true
|
||||||
|
listBatchJob({ pageNum: 1, pageSize: 20 }).then(res => {
|
||||||
|
this.jobList = res.rows || []
|
||||||
|
}).finally(() => {
|
||||||
|
this.jobLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
selectJob(row) {
|
||||||
|
this.currentJob = row
|
||||||
|
this.loadItems(row.id)
|
||||||
|
if ([0, 1, 3].includes(row.status)) {
|
||||||
|
this.startPolling(row.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startPolling(jobId) {
|
||||||
|
this.stopPolling()
|
||||||
|
this.pollTimer = setInterval(() => {
|
||||||
|
this.refreshJob(jobId)
|
||||||
|
}, 1500)
|
||||||
|
this.refreshJob(jobId)
|
||||||
|
},
|
||||||
|
stopPolling() {
|
||||||
|
if (this.pollTimer) {
|
||||||
|
clearInterval(this.pollTimer)
|
||||||
|
this.pollTimer = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refreshJob(jobId) {
|
||||||
|
getBatchJob(jobId).then(res => {
|
||||||
|
this.currentJob = res.data
|
||||||
|
if (this.currentJob && [2, 4, 5, 6].includes(this.currentJob.status)) {
|
||||||
|
this.stopPolling()
|
||||||
|
this.loadItems(jobId)
|
||||||
|
this.loadJobs()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
loadItems(jobId) {
|
||||||
|
this.itemLoading = true
|
||||||
|
listBatchItems(jobId).then(res => {
|
||||||
|
this.items = res.data || []
|
||||||
|
this.selectedItems = []
|
||||||
|
}).finally(() => {
|
||||||
|
this.itemLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleItemSelectionChange(selection) {
|
||||||
|
this.selectedItems = selection
|
||||||
|
},
|
||||||
|
openEdit(row) {
|
||||||
|
this.editForm = Object.assign({}, row)
|
||||||
|
this.editOpen = true
|
||||||
|
},
|
||||||
|
saveItem() {
|
||||||
|
this.savingItem = true
|
||||||
|
updateBatchItem(this.editForm).then(res => {
|
||||||
|
this.$modal.msgSuccess('明细已更新')
|
||||||
|
this.editOpen = false
|
||||||
|
this.loadItems(this.currentJob.id)
|
||||||
|
this.refreshJob(this.currentJob.id)
|
||||||
|
}).finally(() => {
|
||||||
|
this.savingItem = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
removeItem(row) {
|
||||||
|
this.$modal.confirm('确认删除第 ' + row.rowNo + ' 行明细?').then(() => deleteBatchItem(row.id)).then(() => {
|
||||||
|
this.$modal.msgSuccess('删除成功')
|
||||||
|
this.loadItems(this.currentJob.id)
|
||||||
|
this.refreshJob(this.currentJob.id)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
applyBatchAction(onlySelected) {
|
||||||
|
const rows = onlySelected ? this.selectedItems : this.items
|
||||||
|
if (!rows.length || !this.batchActionType) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const label = this.actionLabel(this.batchActionType)
|
||||||
|
this.$modal.confirm('确认将' + (onlySelected ? '已选' : '全部') + '明细的操作类型设置为“' + label + '”?').then(() => {
|
||||||
|
this.batchSaving = true
|
||||||
|
const itemIds = onlySelected ? rows.map(item => item.id) : undefined
|
||||||
|
return updateBatchItemAction(this.currentJob.id, { actionType: this.batchActionType, itemIds })
|
||||||
|
}).then(() => {
|
||||||
|
this.$modal.msgSuccess('批量设置完成')
|
||||||
|
this.loadItems(this.currentJob.id)
|
||||||
|
this.refreshJob(this.currentJob.id)
|
||||||
|
}).finally(() => {
|
||||||
|
this.batchSaving = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleConfirm() {
|
||||||
|
this.$modal.confirm('确认执行当前批量任务?执行后会真正写入库存业务数据。').then(() => {
|
||||||
|
this.confirming = true
|
||||||
|
return confirmBatchJob(this.currentJob.id)
|
||||||
|
}).then(res => {
|
||||||
|
this.currentJob = res.data
|
||||||
|
this.$modal.msgSuccess('执行完成')
|
||||||
|
this.loadItems(this.currentJob.id)
|
||||||
|
this.loadJobs()
|
||||||
|
}).finally(() => {
|
||||||
|
this.confirming = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
statusLabel(status) {
|
||||||
|
return { 0: '已上传', 1: '解析中', 2: '待确认', 3: '执行中', 4: '完成', 5: '失败', 6: '已取消' }[status] || '-'
|
||||||
|
},
|
||||||
|
statusType(status) {
|
||||||
|
return { 2: 'warning', 4: 'success', 5: 'danger', 6: 'info' }[status] || 'info'
|
||||||
|
},
|
||||||
|
sourceLabel(sourceType) {
|
||||||
|
return { excel: 'Excel', image: '图片', file: '文件' }[sourceType] || sourceType || '-'
|
||||||
|
},
|
||||||
|
itemStatusLabel(status) {
|
||||||
|
return { 1: '可执行', 2: '异常', 3: '已执行', 4: '失败' }[status] || '-'
|
||||||
|
},
|
||||||
|
itemStatusType(status) {
|
||||||
|
return { 1: 'success', 2: 'danger', 3: 'success', 4: 'danger' }[status] || 'info'
|
||||||
|
},
|
||||||
|
progressStatus(job) {
|
||||||
|
if (!job) return undefined
|
||||||
|
if (job.status === 4) return 'success'
|
||||||
|
if (job.status === 5) return 'exception'
|
||||||
|
return undefined
|
||||||
|
},
|
||||||
|
actionLabel(action) {
|
||||||
|
return {
|
||||||
|
create_area: '新增库区',
|
||||||
|
create_product: '新增产品',
|
||||||
|
create_in_record: '入库登记',
|
||||||
|
create_out_record: '出库',
|
||||||
|
create_stock_check: '盘库登记',
|
||||||
|
unknown: '未识别'
|
||||||
|
}[action] || action || '-'
|
||||||
|
},
|
||||||
|
productMeta(row) {
|
||||||
|
return [row.brand, row.category, row.spec, row.unit].filter(Boolean).join(' / ')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
canEditItems() {
|
||||||
|
return this.currentJob && ![3, 4].includes(this.currentJob.status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.batch-page {
|
||||||
|
background: #f5f7fb;
|
||||||
|
}
|
||||||
|
.batch-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.card-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2d3d;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #7a8794;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.upload-box {
|
||||||
|
max-width: 620px;
|
||||||
|
}
|
||||||
|
.job-panel {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid #e6ebf2;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fbfcfe;
|
||||||
|
}
|
||||||
|
.job-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.job-no {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2d3d;
|
||||||
|
}
|
||||||
|
.job-meta,
|
||||||
|
.meta {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.job-stats {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.batch-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.error-text,
|
||||||
|
.danger-link {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -198,7 +198,7 @@ export default {
|
|||||||
},
|
},
|
||||||
cancel() { this.open = false; this.reset() },
|
cancel() { this.open = false; this.reset() },
|
||||||
reset() {
|
reset() {
|
||||||
this.form = { id: undefined, inNo: undefined, areaId: undefined, productId: undefined, inOperator: undefined, inType: 1, inNum: undefined, remark: undefined }
|
this.form = { id: undefined, inNo: undefined, areaId: undefined, productId: undefined, inOperator: undefined, inType: 2, inNum: undefined, remark: undefined }
|
||||||
this.resetForm('form')
|
this.resetForm('form')
|
||||||
},
|
},
|
||||||
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
|
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
|
||||||
|
|||||||
49
sql/wms_batch_import.sql
Normal file
49
sql/wms_batch_import.sql
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
-- 批量导入任务表。执行一次即可。
|
||||||
|
create table if not exists wms_batch_job (
|
||||||
|
id bigint not null auto_increment comment '主键',
|
||||||
|
job_no varchar(64) not null comment '任务号',
|
||||||
|
source_type varchar(20) not null comment '来源类型 excel/image/file',
|
||||||
|
biz_type varchar(30) default 'mixed' comment '业务类型',
|
||||||
|
file_name varchar(255) default null comment '原始文件名',
|
||||||
|
file_path varchar(500) default null comment '服务器文件路径',
|
||||||
|
status tinyint not null default 0 comment '状态 0上传完成 1解析中 2待确认 3执行中 4完成 5失败 6取消',
|
||||||
|
progress int not null default 0 comment '进度',
|
||||||
|
total_count int not null default 0 comment '总条数',
|
||||||
|
success_count int not null default 0 comment '成功条数',
|
||||||
|
fail_count int not null default 0 comment '失败条数',
|
||||||
|
error_msg varchar(1000) default null comment '错误信息',
|
||||||
|
create_by varchar(64) default null comment '创建人',
|
||||||
|
create_time datetime default null comment '创建时间',
|
||||||
|
update_time datetime default null comment '更新时间',
|
||||||
|
primary key (id),
|
||||||
|
unique key uk_wms_batch_job_no (job_no)
|
||||||
|
) engine=innodb default charset=utf8mb4 comment='WMS批量导入任务';
|
||||||
|
|
||||||
|
create table if not exists wms_batch_item (
|
||||||
|
id bigint not null auto_increment comment '主键',
|
||||||
|
job_id bigint not null comment '任务ID',
|
||||||
|
row_no int default null comment 'Excel行号',
|
||||||
|
action_type varchar(40) not null comment '操作类型',
|
||||||
|
raw_text text comment '原始内容',
|
||||||
|
parsed_json text comment '解析JSON',
|
||||||
|
status tinyint not null default 1 comment '状态 1校验通过 2校验失败 3已执行 4执行失败',
|
||||||
|
error_msg varchar(1000) default null comment '错误信息',
|
||||||
|
warning_msg varchar(1000) default null comment '提示信息',
|
||||||
|
ref_id bigint default null comment '执行后业务ID',
|
||||||
|
area_name varchar(100) default null comment '库区名称',
|
||||||
|
product_name varchar(200) default null comment '产品名称',
|
||||||
|
brand varchar(100) default null comment '品牌',
|
||||||
|
category varchar(100) default null comment '类别',
|
||||||
|
spec varchar(200) default null comment '规格',
|
||||||
|
unit varchar(50) default null comment '单位',
|
||||||
|
quantity decimal(16,3) default null comment '数量',
|
||||||
|
operator_name varchar(100) default null comment '操作人',
|
||||||
|
type_code tinyint default null comment '入库/出库类型',
|
||||||
|
source_no varchar(100) default null comment '来源单据号',
|
||||||
|
remark varchar(500) default null comment '备注',
|
||||||
|
create_time datetime default null comment '创建时间',
|
||||||
|
update_time datetime default null comment '更新时间',
|
||||||
|
primary key (id),
|
||||||
|
key idx_wms_batch_item_job (job_id),
|
||||||
|
key idx_wms_batch_item_status (status)
|
||||||
|
) engine=innodb default charset=utf8mb4 comment='WMS批量导入明细';
|
||||||
Loading…
Reference in New Issue
Block a user