新增MES库区管理功能,包含免密接口、数据处理逻辑及相关控制器、服务和实体的实现。支持库区的增删改查操作,优化用户体验并增强系统的实时数据同步能力。
This commit is contained in:
@@ -207,6 +207,10 @@ public class ShiroConfig {
|
||||
filterChainDefinitionMap.put("/xslmes/mesXslRawMaterialEntry/anon/**", "anon");
|
||||
// MES原材料卡片免密接口(供桌面端调用)
|
||||
filterChainDefinitionMap.put("/xslmes/mesXslRawMaterialCard/anon/**", "anon");
|
||||
// MES仓库管理只读免密接口(供桌面端下拉筛选调用)
|
||||
filterChainDefinitionMap.put("/xslmes/mesXslWarehouse/anon/**", "anon");
|
||||
// MES库区管理免密接口(供桌面端调用)
|
||||
filterChainDefinitionMap.put("/xslmes/mesXslWarehouseArea/anon/**", "anon");
|
||||
// MES密炼物料管理免密接口(供桌面端调用)
|
||||
filterChainDefinitionMap.put("/mes/material/mixerMaterial/anon/**", "anon");
|
||||
// 系统分类字典免密接口(供桌面端调用)
|
||||
|
||||
@@ -18,12 +18,16 @@ import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialEntry;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslSupplier;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslVehicle;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWarehouse;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWeightRecord;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslCustomerService;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialCardService;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialEntryService;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslSupplierService;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslVehicleService;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslWarehouseAreaService;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslWarehouseService;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslWeightRecordService;
|
||||
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -52,6 +56,8 @@ public class MesXslDesktopAnonController {
|
||||
private final IMesXslWeightRecordService weightRecordService;
|
||||
private final IMesXslRawMaterialEntryService rawMaterialEntryService;
|
||||
private final IMesXslRawMaterialCardService rawMaterialCardService;
|
||||
private final IMesXslWarehouseService warehouseService;
|
||||
private final IMesXslWarehouseAreaService warehouseAreaService;
|
||||
private final MesXslStompNotifyService stompNotify;
|
||||
|
||||
// ═══════════════════════════ 车辆管理 ═══════════════════════════
|
||||
@@ -565,6 +571,162 @@ public class MesXslDesktopAnonController {
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 「重新拆码」专用:按拆码明细 ID 列表批量删除原材料卡片。
|
||||
* dryRun=true 时仅返回匹配数量,不删除;用于桌面端弹窗确认前展示「将清空 N 条卡片」。
|
||||
*/
|
||||
@Operation(summary = "原材料卡片-免密按拆码明细ID批量删除(dryRun=true 仅统计)")
|
||||
@PostMapping("/xslmes/mesXslRawMaterialCard/anon/deleteBySplitDetailIds")
|
||||
public Result<Integer> rawMaterialCardAnonDeleteBySplitDetailIds(
|
||||
@RequestParam(name = "splitDetailIds") String splitDetailIds,
|
||||
@RequestParam(name = "dryRun", required = false, defaultValue = "false") Boolean dryRun) {
|
||||
if (oConvertUtils.isEmpty(splitDetailIds)) {
|
||||
return Result.OK(0);
|
||||
}
|
||||
java.util.List<String> idList = java.util.Arrays.stream(splitDetailIds.split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.distinct()
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
if (idList.isEmpty()) {
|
||||
return Result.OK(0);
|
||||
}
|
||||
long count = rawMaterialCardService.lambdaQuery()
|
||||
.in(MesXslRawMaterialCard::getSplitDetailId, idList)
|
||||
.count();
|
||||
if (Boolean.TRUE.equals(dryRun)) {
|
||||
return Result.OK((int) count);
|
||||
}
|
||||
boolean ok = rawMaterialCardService.lambdaUpdate()
|
||||
.in(MesXslRawMaterialCard::getSplitDetailId, idList)
|
||||
.remove();
|
||||
if (ok) {
|
||||
stompNotify.publishRawMaterialCardChanged("batchDelete", String.join(",", idList));
|
||||
}
|
||||
return Result.OK((int) count);
|
||||
}
|
||||
|
||||
// ═══════════════════════════ 仓库管理(只读,供桌面端下拉选取) ═══════════════════════════
|
||||
|
||||
@Operation(summary = "仓库-免密分页列表查询(供桌面端筛选使用)")
|
||||
@GetMapping("/xslmes/mesXslWarehouse/anon/list")
|
||||
public Result<IPage<MesXslWarehouse>> warehouseAnonList(
|
||||
MesXslWarehouse mesXslWarehouse,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "200") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<MesXslWarehouse> qw = QueryGenerator.initQueryWrapper(mesXslWarehouse, req.getParameterMap());
|
||||
qw.orderByAsc("warehouse_name");
|
||||
IPage<MesXslWarehouse> page = warehouseService.page(new Page<>(pageNo, pageSize), qw);
|
||||
return Result.OK(page);
|
||||
}
|
||||
|
||||
// ═══════════════════════════ 库区管理 ═══════════════════════════
|
||||
|
||||
@Operation(summary = "库区-免密分页列表查询")
|
||||
@GetMapping("/xslmes/mesXslWarehouseArea/anon/list")
|
||||
public Result<IPage<MesXslWarehouseArea>> warehouseAreaAnonList(
|
||||
MesXslWarehouseArea mesXslWarehouseArea,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<MesXslWarehouseArea> qw = QueryGenerator.initQueryWrapper(mesXslWarehouseArea, req.getParameterMap());
|
||||
IPage<MesXslWarehouseArea> page = warehouseAreaService.page(new Page<>(pageNo, pageSize), qw);
|
||||
return Result.OK(page);
|
||||
}
|
||||
|
||||
@Operation(summary = "库区-免密通过id查询")
|
||||
@GetMapping("/xslmes/mesXslWarehouseArea/anon/queryById")
|
||||
public Result<MesXslWarehouseArea> warehouseAreaAnonQueryById(@RequestParam(name = "id") String id) {
|
||||
MesXslWarehouseArea entity = warehouseAreaService.getById(id);
|
||||
return entity != null ? Result.OK(entity) : Result.error("未找到对应数据");
|
||||
}
|
||||
|
||||
@Operation(summary = "库区-免密添加")
|
||||
@PostMapping("/xslmes/mesXslWarehouseArea/anon/add")
|
||||
public Result<String> warehouseAreaAnonAdd(@RequestBody MesXslWarehouseArea entity) {
|
||||
if (oConvertUtils.isEmpty(entity.getAreaCode()) || entity.getAreaCode().trim().isEmpty()) {
|
||||
return Result.error("库区编码不能为空");
|
||||
}
|
||||
if (warehouseAreaService.existsSameAreaCode(entity.getAreaCode().trim(), null)) {
|
||||
return Result.error("库区编码已存在,不允许重复");
|
||||
}
|
||||
if (entity.getStatus() == null || entity.getStatus().isEmpty()) {
|
||||
entity.setStatus("0");
|
||||
}
|
||||
warehouseAreaService.save(entity);
|
||||
stompNotify.publishWarehouseAreaChanged("add", entity.getId());
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
@Operation(summary = "库区-免密编辑")
|
||||
@RequestMapping(value = "/xslmes/mesXslWarehouseArea/anon/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
public Result<String> warehouseAreaAnonEdit(@RequestBody MesXslWarehouseArea entity) {
|
||||
if (oConvertUtils.isEmpty(entity.getId())) {
|
||||
return Result.error("主键不能为空");
|
||||
}
|
||||
if (oConvertUtils.isEmpty(entity.getAreaCode()) || entity.getAreaCode().trim().isEmpty()) {
|
||||
return Result.error("库区编码不能为空");
|
||||
}
|
||||
if (warehouseAreaService.existsSameAreaCode(entity.getAreaCode().trim(), entity.getId())) {
|
||||
return Result.error("库区编码已存在,不允许重复");
|
||||
}
|
||||
boolean ok = warehouseAreaService.updateById(entity);
|
||||
if (!ok) {
|
||||
return Result.error("数据已被他人修改,请刷新后重试");
|
||||
}
|
||||
stompNotify.publishWarehouseAreaChanged("edit", entity.getId());
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
@Operation(summary = "库区-免密删除")
|
||||
@DeleteMapping("/xslmes/mesXslWarehouseArea/anon/delete")
|
||||
public Result<String> warehouseAreaAnonDelete(@RequestParam(name = "id") String id) {
|
||||
warehouseAreaService.removeById(id);
|
||||
stompNotify.publishWarehouseAreaChanged("delete", id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
@Operation(summary = "库区-免密批量删除")
|
||||
@DeleteMapping("/xslmes/mesXslWarehouseArea/anon/deleteBatch")
|
||||
public Result<String> warehouseAreaAnonDeleteBatch(@RequestParam(name = "ids") String ids) {
|
||||
warehouseAreaService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
stompNotify.publishWarehouseAreaChanged("batchDelete", ids);
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
@Operation(summary = "库区-免密启用/停用")
|
||||
@PostMapping("/xslmes/mesXslWarehouseArea/anon/updateStatus")
|
||||
public Result<String> warehouseAreaAnonUpdateStatus(
|
||||
@RequestParam(name = "id") String id,
|
||||
@RequestParam(name = "status") String status) {
|
||||
if (!"0".equals(status) && !"1".equals(status)) {
|
||||
return Result.error("状态参数非法");
|
||||
}
|
||||
boolean ok = warehouseAreaService.lambdaUpdate()
|
||||
.eq(MesXslWarehouseArea::getId, id)
|
||||
.set(MesXslWarehouseArea::getStatus, status)
|
||||
.update();
|
||||
if (ok) {
|
||||
stompNotify.publishWarehouseAreaChanged("status", id);
|
||||
}
|
||||
return ok ? Result.OK("操作成功") : Result.error("操作失败");
|
||||
}
|
||||
|
||||
@Operation(summary = "库区-免密校验库区编码是否重复")
|
||||
@GetMapping("/xslmes/mesXslWarehouseArea/anon/checkAreaCode")
|
||||
public Result<String> warehouseAreaAnonCheckAreaCode(
|
||||
@RequestParam(name = "areaCode") String areaCode,
|
||||
@RequestParam(name = "dataId", required = false) String dataId) {
|
||||
if (oConvertUtils.isEmpty(areaCode) || areaCode.trim().isEmpty()) {
|
||||
return Result.OK("该值可用!");
|
||||
}
|
||||
if (warehouseAreaService.existsSameAreaCode(areaCode.trim(), dataId)) {
|
||||
return Result.error("该库区编码已存在");
|
||||
}
|
||||
return Result.OK("该值可用!");
|
||||
}
|
||||
|
||||
// ─────────────────────────── 车辆私有辅助 ────────────────────────────
|
||||
|
||||
private void applyWeightBillType(MesXslWeightRecord record) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.jeecg.modules.xslmes.service.IMesXslRawMaterialEntryService;
|
||||
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -82,6 +83,18 @@ public class MesXslRawMaterialEntryController extends JeecgController<MesXslRawM
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "原料入场记录-批量结存入库")
|
||||
@Operation(summary = "原料入场记录-批量结存入库:将选中记录的入库结存改为「是」")
|
||||
@RequiresPermissions("xslmes:mes_xsl_raw_material_entry:stockIn")
|
||||
@PutMapping(value = "/batchStockIn")
|
||||
public Result<String> batchStockIn(@RequestParam(name = "ids", required = true) String ids) {
|
||||
UpdateWrapper<MesXslRawMaterialEntry> uw = new UpdateWrapper<>();
|
||||
uw.in("id", Arrays.asList(ids.split(","))).set("stock_balance", "1");
|
||||
mesXslRawMaterialEntryService.update(uw);
|
||||
stompNotify.publishRawMaterialEntryChanged("batchStockIn", ids);
|
||||
return Result.OK("结存入库成功!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "原料入场记录-批量删除")
|
||||
@Operation(summary = "原料入场记录-批量删除")
|
||||
@RequiresPermissions("xslmes:mes_xsl_raw_material_entry:deleteBatch")
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
package org.jeecg.modules.xslmes.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslWarehouseAreaService;
|
||||
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MES 库区管理
|
||||
*/
|
||||
@Tag(name = "MES库区管理")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/mesXslWarehouseArea")
|
||||
@Slf4j
|
||||
public class MesXslWarehouseAreaController extends JeecgController<MesXslWarehouseArea, IMesXslWarehouseAreaService> {
|
||||
|
||||
@Autowired
|
||||
private IMesXslWarehouseAreaService mesXslWarehouseAreaService;
|
||||
|
||||
@Autowired
|
||||
private MesXslStompNotifyService stompNotify;
|
||||
|
||||
@Operation(summary = "MES库区管理-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<MesXslWarehouseArea>> queryPageList(
|
||||
MesXslWarehouseArea mesXslWarehouseArea,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<MesXslWarehouseArea> queryWrapper = QueryGenerator.initQueryWrapper(mesXslWarehouseArea, req.getParameterMap());
|
||||
Page<MesXslWarehouseArea> page = new Page<>(pageNo, pageSize);
|
||||
IPage<MesXslWarehouseArea> pageList = mesXslWarehouseAreaService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
@AutoLog(value = "MES库区管理-添加")
|
||||
@Operation(summary = "MES库区管理-添加")
|
||||
@RequiresPermissions("xslmes:mes_xsl_warehouse_area:add")
|
||||
@PostMapping(value = "/add")
|
||||
public Result<String> add(@RequestBody MesXslWarehouseArea mesXslWarehouseArea) {
|
||||
if (mesXslWarehouseAreaService.existsSameAreaCode(mesXslWarehouseArea.getAreaCode(), null)) {
|
||||
return Result.error("库区编码已存在");
|
||||
}
|
||||
if (mesXslWarehouseArea.getStatus() == null || mesXslWarehouseArea.getStatus().isEmpty()) {
|
||||
mesXslWarehouseArea.setStatus("0");
|
||||
}
|
||||
mesXslWarehouseAreaService.save(mesXslWarehouseArea);
|
||||
stompNotify.publishWarehouseAreaChanged("add", mesXslWarehouseArea.getId());
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "MES库区管理-编辑")
|
||||
@Operation(summary = "MES库区管理-编辑")
|
||||
@RequiresPermissions("xslmes:mes_xsl_warehouse_area:edit")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody MesXslWarehouseArea mesXslWarehouseArea) {
|
||||
if (mesXslWarehouseAreaService.existsSameAreaCode(mesXslWarehouseArea.getAreaCode(), mesXslWarehouseArea.getId())) {
|
||||
return Result.error("库区编码已存在");
|
||||
}
|
||||
mesXslWarehouseAreaService.updateById(mesXslWarehouseArea);
|
||||
stompNotify.publishWarehouseAreaChanged("edit", mesXslWarehouseArea.getId());
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "MES库区管理-停用/启用")
|
||||
@Operation(summary = "MES库区管理-停用/启用(字典 xslmes_unit_status:0启用 1停用)")
|
||||
@RequiresPermissions("xslmes:mes_xsl_warehouse_area:updateStatus")
|
||||
@PostMapping(value = "/updateStatus")
|
||||
public Result<String> updateStatus(
|
||||
@RequestParam(name = "id", required = true) String id,
|
||||
@RequestParam(name = "status", required = true) String status) {
|
||||
if (!"0".equals(status) && !"1".equals(status)) {
|
||||
return Result.error("状态参数非法");
|
||||
}
|
||||
boolean ok = mesXslWarehouseAreaService.lambdaUpdate()
|
||||
.eq(MesXslWarehouseArea::getId, id)
|
||||
.set(MesXslWarehouseArea::getStatus, status)
|
||||
.update();
|
||||
stompNotify.publishWarehouseAreaChanged("edit", id);
|
||||
return ok ? Result.OK("操作成功") : Result.error("操作失败");
|
||||
}
|
||||
|
||||
@Operation(summary = "校验库区编码是否重复(同租户,dataId 为编辑时当前主键)")
|
||||
@GetMapping(value = "/checkAreaCode")
|
||||
public Result<String> checkAreaCode(
|
||||
@RequestParam(name = "areaCode", required = true) String areaCode,
|
||||
@RequestParam(name = "dataId", required = false) String dataId) {
|
||||
if (oConvertUtils.isEmpty(areaCode) || areaCode.trim().isEmpty()) {
|
||||
return Result.OK("该值可用!");
|
||||
}
|
||||
if (mesXslWarehouseAreaService.existsSameAreaCode(areaCode, dataId)) {
|
||||
return Result.error("该库区编码已存在");
|
||||
}
|
||||
return Result.OK("该值可用!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "MES库区管理-批量添加")
|
||||
@Operation(summary = "MES库区管理-批量添加(为指定仓库一次性创建多条库区)")
|
||||
@RequiresPermissions("xslmes:mes_xsl_warehouse_area:add")
|
||||
@PostMapping(value = "/batchAdd")
|
||||
public Result<String> batchAdd(@RequestBody List<MesXslWarehouseArea> list) {
|
||||
if (list == null || list.isEmpty()) {
|
||||
return Result.error("请至少添加一条库区记录");
|
||||
}
|
||||
for (MesXslWarehouseArea area : list) {
|
||||
if (oConvertUtils.isEmpty(area.getAreaCode()) || area.getAreaCode().trim().isEmpty()) {
|
||||
return Result.error("库区编码不能为空");
|
||||
}
|
||||
if (mesXslWarehouseAreaService.existsSameAreaCode(area.getAreaCode(), null)) {
|
||||
return Result.error("库区编码「" + area.getAreaCode() + "」已存在,请修改后重试");
|
||||
}
|
||||
if (area.getStatus() == null || area.getStatus().isEmpty()) {
|
||||
area.setStatus("0");
|
||||
}
|
||||
}
|
||||
mesXslWarehouseAreaService.saveBatch(list);
|
||||
for (MesXslWarehouseArea area : list) {
|
||||
stompNotify.publishWarehouseAreaChanged("add", area.getId());
|
||||
}
|
||||
return Result.OK("批量添加成功,共创建 " + list.size() + " 个库区!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "MES库区管理-删除")
|
||||
@Operation(summary = "MES库区管理-删除")
|
||||
@RequiresPermissions("xslmes:mes_xsl_warehouse_area:delete")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
||||
mesXslWarehouseAreaService.removeById(id);
|
||||
stompNotify.publishWarehouseAreaChanged("delete", id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "MES库区管理-批量删除")
|
||||
@Operation(summary = "MES库区管理-批量删除")
|
||||
@RequiresPermissions("xslmes:mes_xsl_warehouse_area:deleteBatch")
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
|
||||
mesXslWarehouseAreaService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
stompNotify.publishWarehouseAreaChanged("batchDelete", ids);
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
@Operation(summary = "MES库区管理-通过id查询")
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<MesXslWarehouseArea> queryById(@RequestParam(name = "id", required = true) String id) {
|
||||
MesXslWarehouseArea entity = mesXslWarehouseAreaService.getById(id);
|
||||
if (entity == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(entity);
|
||||
}
|
||||
|
||||
@RequiresPermissions("xslmes:mes_xsl_warehouse_area:exportXls")
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, MesXslWarehouseArea mesXslWarehouseArea) {
|
||||
return super.exportXls(request, mesXslWarehouseArea, MesXslWarehouseArea.class, "MES库区管理");
|
||||
}
|
||||
|
||||
@RequiresPermissions("xslmes:mes_xsl_warehouse_area:importExcel")
|
||||
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
|
||||
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
|
||||
return super.importExcel(request, response, MesXslWarehouseArea.class);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWeightRecord;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialEntryService;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslWeightRecordService;
|
||||
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -23,8 +24,12 @@ import org.springframework.web.servlet.ModelAndView;
|
||||
import java.math.BigDecimal;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 地磅数据记录
|
||||
@@ -39,6 +44,8 @@ public class MesXslWeightRecordController extends JeecgController<MesXslWeightRe
|
||||
private IMesXslWeightRecordService mesXslWeightRecordService;
|
||||
@Autowired
|
||||
private MesXslStompNotifyService stompNotifyService;
|
||||
@Autowired
|
||||
private IMesXslRawMaterialEntryService rawMaterialEntryService;
|
||||
|
||||
@Operation(summary = "地磅数据记录-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
@@ -50,6 +57,7 @@ public class MesXslWeightRecordController extends JeecgController<MesXslWeightRe
|
||||
queryWrapper.orderByDesc("create_time");
|
||||
Page<MesXslWeightRecord> page = new Page<>(pageNo, pageSize);
|
||||
IPage<MesXslWeightRecord> pageList = mesXslWeightRecordService.page(page, queryWrapper);
|
||||
fillEnteredWeight(pageList.getRecords());
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
@@ -112,6 +120,7 @@ public class MesXslWeightRecordController extends JeecgController<MesXslWeightRe
|
||||
if (record == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
fillEnteredWeight(Collections.singletonList(record));
|
||||
return Result.OK(record);
|
||||
}
|
||||
|
||||
@@ -156,4 +165,28 @@ public class MesXslWeightRecordController extends JeecgController<MesXslWeightRe
|
||||
record.setMaterialType("1");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 给一批磅单记录批量填充「已入场重量」(transient 字段,不入库)。
|
||||
* 数据来源:所有引用本榜单(bill_no 匹配)的原料入场记录的拆码明细的 (份数×每份重量) 累计。
|
||||
* 实现上为避免 N+1,先收集所有 billNo,再一次 IN 查询累计。
|
||||
*/
|
||||
private void fillEnteredWeight(List<MesXslWeightRecord> records) {
|
||||
if (records == null || records.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<String> billNos = records.stream()
|
||||
.map(MesXslWeightRecord::getBillNo)
|
||||
.filter(s -> s != null && !s.isBlank())
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (billNos.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, BigDecimal> sumMap = rawMaterialEntryService.sumEnteredWeightByBillNos(billNos);
|
||||
for (MesXslWeightRecord r : records) {
|
||||
BigDecimal v = (r.getBillNo() == null) ? null : sumMap.get(r.getBillNo());
|
||||
r.setEnteredWeight(v != null ? v : BigDecimal.ZERO);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,14 @@ public class MesXslRawMaterialCard implements Serializable {
|
||||
@Schema(description = "条码")
|
||||
private String barcode;
|
||||
|
||||
/**
|
||||
* 关联的拆码明细行 ID(GUID)。
|
||||
* 「生成原材料卡片」时由桌面端把对应 RawMaterialSplitDetailItem.Id 写入;
|
||||
* 「重新拆码」时按入场记录 portion_detail_ids 反查、IN 批量清除关联卡片。
|
||||
*/
|
||||
@Schema(description = "关联的拆码明细行 ID(GUID)")
|
||||
private String splitDetailId;
|
||||
|
||||
@Excel(name = "批次号", width = 20)
|
||||
@Schema(description = "批次号")
|
||||
private String batchNo;
|
||||
|
||||
@@ -102,6 +102,32 @@ public class MesXslRawMaterialEntry implements Serializable {
|
||||
@Schema(description = "每份包数(支持多行拆码明细拼接)")
|
||||
private String portionPackages;
|
||||
|
||||
/**
|
||||
* 拆码明细库位拼接(以 / 分隔,末尾带 /,如 1F-A01/1F-A02/)。
|
||||
* 与 warehouseLocation(基础资料整票级单值)区分:本字段保存每行明细各自的库位,
|
||||
* 用于桌面端「新增原料入场记录」重新打开后回填每行库位。
|
||||
*/
|
||||
@Excel(name = "明细库位", width = 24)
|
||||
@Schema(description = "拆码明细库位拼接(以 / 分隔,末尾带 /,如 1F-A01/1F-A02/)")
|
||||
private String portionWarehouseLocations;
|
||||
|
||||
/**
|
||||
* 拆码明细每行的 GUID 拼接(以 / 分隔,末尾带 /)。
|
||||
* 与 total_portions / portion_weight / portion_packages / portion_warehouse_locations
|
||||
* 按相同分隔规则、行序对齐,供「重新拆码」时按 IN 批量删除原材料卡片。
|
||||
*/
|
||||
@Schema(description = "拆码明细每行的 GUID 拼接(以 / 分隔,末尾带 /)")
|
||||
private String portionDetailIds;
|
||||
|
||||
/**
|
||||
* 拆码明细行级「已生成卡片」标志拼接(以 / 分隔,末尾带 /,1=已生成 0=未生成)。
|
||||
* 桌面端 GenerateRawMaterialCardsAsync 按此字段过滤待生成行(HasCard==false 的行才参与生成),
|
||||
* 避免靠 print_flag 推断导致「保存后新增行被误判为已打印」的问题。
|
||||
* 历史记录留空时桌面端降级使用 print_flag 推断,与旧行为兼容。
|
||||
*/
|
||||
@Schema(description = "拆码明细行级「已生成卡片」标志拼接(以 / 分隔,1=已生成 0=未生成,如 1/1/0/)")
|
||||
private String portionCardFlags;
|
||||
|
||||
@Excel(name = "检测结果", width = 12, dicCode = "xslmes_test_result")
|
||||
@Dict(dicCode = "xslmes_test_result")
|
||||
@Schema(description = "检测结果(字典 xslmes_test_result:0未检 1合格 2不合格)")
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.jeecg.modules.xslmes.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
import org.jeecg.common.system.base.entity.JeecgEntity;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* MES 库区管理
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("mes_xsl_warehouse_area")
|
||||
@Schema(description = "MES库区管理")
|
||||
public class MesXslWarehouseArea extends JeecgEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Excel(name = "库区编码", width = 18)
|
||||
@Schema(description = "库区编码(同租户唯一)")
|
||||
private String areaCode;
|
||||
|
||||
@Excel(name = "库区名称", width = 22)
|
||||
@Schema(description = "库区名称")
|
||||
private String areaName;
|
||||
|
||||
@Schema(description = "所属仓库ID")
|
||||
private String warehouseId;
|
||||
|
||||
@Excel(name = "所属仓库", width = 22)
|
||||
@Schema(description = "所属仓库名称(冗余,由仓库带出)")
|
||||
private String warehouseName;
|
||||
|
||||
@Excel(name = "仓库分类", width = 14, dictTable = "sys_category", dicText = "name", dicCode = "id")
|
||||
@Dict(dictTable = "sys_category", dicText = "name", dicCode = "id")
|
||||
@Schema(description = "仓库分类(sys_category.id,由仓库自动带出,根编码 XSLMES_WH)")
|
||||
private String warehouseCategory;
|
||||
|
||||
@Excel(name = "最大存放量", width = 14)
|
||||
@Schema(description = "最大存放量")
|
||||
private Integer maxCapacity;
|
||||
|
||||
@Excel(name = "实际存放量", width = 14)
|
||||
@Schema(description = "实际存放量")
|
||||
private Integer actualCapacity;
|
||||
|
||||
@Excel(name = "备注", width = 30)
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Excel(name = "状态", width = 10, dicCode = "xslmes_unit_status")
|
||||
@Dict(dicCode = "xslmes_unit_status")
|
||||
@Schema(description = "状态:0启用 1停用")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "删除状态(0正常 1已删除)")
|
||||
@TableLogic
|
||||
private Integer delFlag;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
private Integer tenantId;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.jeecg.modules.xslmes.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
@@ -118,4 +119,14 @@ public class MesXslWeightRecord extends JeecgEntity implements Serializable {
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
private Integer tenantId;
|
||||
|
||||
/**
|
||||
* 已入场重量(KG)—— 实时计算,不落库。
|
||||
* 数据来源:所有引用本榜单(bill_no 匹配)的原料入场记录的拆码明细。
|
||||
* 公式:每条 entry 按 "x/y/z/" 拆分 totalPortions 和 portionWeight,
|
||||
* 逐位 (份数 × 每份重量) 累加;再把所有匹配 entry 的结果再次累加。
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "已入场重量(KG),由原料入场记录的拆码明细实时累计")
|
||||
private BigDecimal enteredWeight;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.jeecg.modules.xslmes.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
|
||||
|
||||
/**
|
||||
* MES 库区管理 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface MesXslWarehouseAreaMapper extends BaseMapper<MesXslWarehouseArea> {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.jeecg.modules.xslmes.mapper.MesXslWarehouseAreaMapper">
|
||||
</mapper>
|
||||
@@ -3,6 +3,10 @@ package org.jeecg.modules.xslmes.service;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialEntry;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: 原料入场记录
|
||||
* @Author: jeecg-boot
|
||||
@@ -19,4 +23,15 @@ public interface IMesXslRawMaterialEntryService extends IService<MesXslRawMateri
|
||||
* @return 生成的条码字符串
|
||||
*/
|
||||
String generateBarcode(String materialCode);
|
||||
|
||||
/**
|
||||
* 按榜单号批量统计「已入场重量」。
|
||||
* <p>
|
||||
* 计算口径:对每条匹配 bill_no 的入场记录,将 totalPortions 和 portionWeight 按 "x/y/z/" 拆分,
|
||||
* 逐位 (份数 × 每份重量) 累加,再把所有匹配 entry 的结果加在一起。
|
||||
*
|
||||
* @param billNos 榜单号集合(去重前后都可)
|
||||
* @return billNo -> 累计已入场重量;查不到的 billNo 不会出现在 map 中
|
||||
*/
|
||||
Map<String, BigDecimal> sumEnteredWeightByBillNos(Collection<String> billNos);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.jeecg.modules.xslmes.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
|
||||
|
||||
/**
|
||||
* MES 库区管理
|
||||
*/
|
||||
public interface IMesXslWarehouseAreaService extends IService<MesXslWarehouseArea> {
|
||||
|
||||
/**
|
||||
* 同租户下是否已存在相同库区编码(编辑时传 excludeId 排除自身)
|
||||
*/
|
||||
boolean existsSameAreaCode(String areaCode, String excludeId);
|
||||
}
|
||||
@@ -65,6 +65,11 @@ public class MesXslStompNotifyService {
|
||||
publish("/topic/sync/sys-dicts", "SYS_DICT_CHANGED", "dictId", dictId, action);
|
||||
}
|
||||
|
||||
/** 广播库区数据变更事件到 /topic/sync/mes-warehouse-areas */
|
||||
public void publishWarehouseAreaChanged(String action, String warehouseAreaId) {
|
||||
publish("/topic/sync/mes-warehouse-areas", "MES_WAREHOUSE_AREA_CHANGED", "warehouseAreaId", warehouseAreaId, action);
|
||||
}
|
||||
|
||||
// ─────────────────────────── 私有辅助 ────────────────────────────
|
||||
|
||||
private void publish(String topic, String cmd, String idKey, String idValue, String action) {
|
||||
|
||||
@@ -7,8 +7,17 @@ import org.jeecg.modules.xslmes.mapper.MesXslRawMaterialEntryMapper;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialEntryService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @Description: 原料入场记录
|
||||
@@ -33,4 +42,76 @@ public class MesXslRawMaterialEntryServiceImpl
|
||||
|
||||
return prefix + String.format("%03d", count + 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, BigDecimal> sumEnteredWeightByBillNos(Collection<String> billNos) {
|
||||
if (billNos == null || billNos.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
// 1) 去重 + 过滤空值
|
||||
Set<String> distinct = billNos.stream()
|
||||
.filter(s -> s != null && !s.isBlank())
|
||||
.collect(Collectors.toCollection(HashSet::new));
|
||||
if (distinct.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
// 2) 一次查询拿到所有匹配 entry 的 (billNo, totalPortions, portionWeight)
|
||||
// 只 select 三列,避免拉无关大字段
|
||||
LambdaQueryWrapper<MesXslRawMaterialEntry> qw = new LambdaQueryWrapper<>();
|
||||
qw.in(MesXslRawMaterialEntry::getBillNo, distinct)
|
||||
.select(MesXslRawMaterialEntry::getBillNo,
|
||||
MesXslRawMaterialEntry::getTotalPortions,
|
||||
MesXslRawMaterialEntry::getPortionWeight);
|
||||
List<MesXslRawMaterialEntry> rows = this.list(qw);
|
||||
|
||||
// 3) Java 端按 "x/y/z/" 拆分并逐位 (份数 × 每份重量) 累加
|
||||
Map<String, BigDecimal> result = new HashMap<>();
|
||||
for (MesXslRawMaterialEntry row : rows) {
|
||||
BigDecimal sub = sumOnePackPositions(row.getTotalPortions(), row.getPortionWeight());
|
||||
if (sub.compareTo(BigDecimal.ZERO) == 0) continue;
|
||||
result.merge(row.getBillNo(), sub, BigDecimal::add);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把一条入场记录的 totalPortions/portionWeight 按 "x/y/z/" 解析后,逐位 (份数 × 每份重量) 累加。
|
||||
* 任一位置解析失败或缺失,跳过该位置(不抛异常,确保前端展示降级可用)。
|
||||
*/
|
||||
private static BigDecimal sumOnePackPositions(String totalPortions, String portionWeight) {
|
||||
String[] portionsArr = splitJoined(totalPortions);
|
||||
String[] weightsArr = splitJoined(portionWeight);
|
||||
if (portionsArr.length == 0 || weightsArr.length == 0) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
int n = Math.min(portionsArr.length, weightsArr.length);
|
||||
BigDecimal sum = BigDecimal.ZERO;
|
||||
for (int i = 0; i < n; i++) {
|
||||
BigDecimal portions = tryParseBigDecimal(portionsArr[i]);
|
||||
BigDecimal weight = tryParseBigDecimal(weightsArr[i]);
|
||||
if (portions == null || weight == null) continue;
|
||||
sum = sum.add(portions.multiply(weight));
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
private static String[] splitJoined(String value) {
|
||||
if (value == null || value.isBlank()) return new String[0];
|
||||
// 兼容 "x/y/z" 与 "x/y/z/" 两种格式,过滤空段
|
||||
return java.util.Arrays.stream(value.split("/"))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
private static BigDecimal tryParseBigDecimal(String s) {
|
||||
if (s == null || s.isBlank()) return null;
|
||||
try {
|
||||
return new BigDecimal(s.trim());
|
||||
} catch (NumberFormatException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.jeecg.modules.xslmes.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
|
||||
import org.jeecg.modules.xslmes.mapper.MesXslWarehouseAreaMapper;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslWarehouseAreaService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* MES 库区管理
|
||||
*/
|
||||
@Service
|
||||
public class MesXslWarehouseAreaServiceImpl extends ServiceImpl<MesXslWarehouseAreaMapper, MesXslWarehouseArea> implements IMesXslWarehouseAreaService {
|
||||
|
||||
@Override
|
||||
public boolean existsSameAreaCode(String areaCode, String excludeId) {
|
||||
if (StringUtils.isBlank(areaCode)) {
|
||||
return false;
|
||||
}
|
||||
String code = areaCode.trim();
|
||||
return this.lambdaQuery()
|
||||
.eq(MesXslWarehouseArea::getAreaCode, code)
|
||||
.ne(StringUtils.isNotBlank(excludeId), MesXslWarehouseArea::getId, excludeId)
|
||||
.count() > 0L;
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ public class SysCategoryServiceImpl extends ServiceImpl<SysCategoryMapper, SysCa
|
||||
|
||||
@Override
|
||||
public void addSysCategory(SysCategory sysCategory) {
|
||||
String categoryCode = "";
|
||||
String categoryPid = ISysCategoryService.ROOT_PID_VALUE;
|
||||
String parentCode = null;
|
||||
if(oConvertUtils.isNotEmpty(sysCategory.getPid())){
|
||||
@@ -47,11 +46,23 @@ public class SysCategoryServiceImpl extends ServiceImpl<SysCategoryMapper, SysCa
|
||||
}
|
||||
}
|
||||
}
|
||||
// 代码逻辑说明: 分类字典编码规则生成器做成公用配置
|
||||
JSONObject formData = new JSONObject();
|
||||
formData.put("pid",categoryPid);
|
||||
categoryCode = (String) FillRuleUtil.executeRule(FillRuleConstant.CATEGORY,formData);
|
||||
sysCategory.setCode(categoryCode);
|
||||
// 编码处理:
|
||||
// ① 前端传入了编码 → 使用前端传入值(允许用户自定义编码);
|
||||
// ② 前端未传 → 按规则自动生成(保留原行为);
|
||||
// ③ 防止重复:自定义编码先查一次是否已存在,存在则抛业务异常。
|
||||
if(oConvertUtils.isEmpty(sysCategory.getCode())){
|
||||
JSONObject formData = new JSONObject();
|
||||
formData.put("pid",categoryPid);
|
||||
String categoryCode = (String) FillRuleUtil.executeRule(FillRuleConstant.CATEGORY,formData);
|
||||
sysCategory.setCode(categoryCode);
|
||||
} else {
|
||||
String customCode = sysCategory.getCode().trim();
|
||||
long exists = this.count(new LambdaQueryWrapper<SysCategory>().eq(SysCategory::getCode, customCode));
|
||||
if(exists > 0){
|
||||
throw new JeecgBootException("分类编码【"+customCode+"】已存在,请更换!");
|
||||
}
|
||||
sysCategory.setCode(customCode);
|
||||
}
|
||||
sysCategory.setPid(categoryPid);
|
||||
baseMapper.insert(sysCategory);
|
||||
}
|
||||
@@ -68,6 +79,18 @@ public class SysCategoryServiceImpl extends ServiceImpl<SysCategoryMapper, SysCa
|
||||
baseMapper.updateById(parent);
|
||||
}
|
||||
}
|
||||
// 编辑模式编码重复校验:用户改成已存在的编码(非自身记录)时拒绝保存。
|
||||
// 编码为空时跳过校验(保留旧值由数据库层面控制)。
|
||||
if(oConvertUtils.isNotEmpty(sysCategory.getCode())){
|
||||
String newCode = sysCategory.getCode().trim();
|
||||
long exists = this.count(new LambdaQueryWrapper<SysCategory>()
|
||||
.eq(SysCategory::getCode, newCode)
|
||||
.ne(SysCategory::getId, sysCategory.getId()));
|
||||
if(exists > 0){
|
||||
throw new JeecgBootException("分类编码【"+newCode+"】已存在,请更换!");
|
||||
}
|
||||
sysCategory.setCode(newCode);
|
||||
}
|
||||
baseMapper.updateById(sysCategory);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
-- 库区管理:建表 + 菜单权限(幂等)
|
||||
|
||||
-- ===================== 1. 建表 =====================
|
||||
CREATE TABLE IF NOT EXISTS `mes_xsl_warehouse_area` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键',
|
||||
`area_code` varchar(100) DEFAULT NULL COMMENT '库区编码(同租户唯一)',
|
||||
`area_name` varchar(200) DEFAULT NULL COMMENT '库区名称',
|
||||
`warehouse_id` varchar(32) DEFAULT NULL COMMENT '所属仓库ID',
|
||||
`warehouse_name` varchar(200) DEFAULT NULL COMMENT '所属仓库名称(冗余)',
|
||||
`warehouse_category` varchar(36) DEFAULT NULL COMMENT '仓库分类(sys_category.id,由仓库带出,根编码 XSLMES_WH)',
|
||||
`max_capacity` int DEFAULT NULL COMMENT '最大存放量',
|
||||
`actual_capacity` int DEFAULT NULL COMMENT '实际存放量',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
`status` varchar(10) DEFAULT '0' COMMENT '状态(字典 xslmes_unit_status:0启用 1停用)',
|
||||
`create_by` varchar(50) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` varchar(50) DEFAULT NULL COMMENT '更新人',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`del_flag` int DEFAULT 0 COMMENT '删除标记(0正常 1已删除)',
|
||||
`tenant_id` int DEFAULT 1002 COMMENT '租户ID',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_wa_warehouse_id` (`warehouse_id`),
|
||||
KEY `idx_wa_area_code` (`area_code`),
|
||||
KEY `idx_wa_warehouse_category` (`warehouse_category`),
|
||||
KEY `idx_wa_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES库区管理';
|
||||
|
||||
-- ===================== 2. 菜单权限(父菜单:MES XSL 1900000000000000300)=====================
|
||||
INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`)
|
||||
SELECT '1900000000000000550', '1900000000000000300', '库区管理', '/xslmes/mesXslWarehouseArea', 'xslmes/mesXslWarehouseArea/MesXslWarehouseAreaList', 1, NULL, NULL, 1, NULL, '0', 13.00, 0, 'ant-design:appstore-outlined', 0, 1, 0, 0, '库区管理', 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000550');
|
||||
|
||||
INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`)
|
||||
SELECT '1900000000000000551', '1900000000000000550', '添加', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_warehouse_area:add', '1', 1.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000551');
|
||||
|
||||
INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`)
|
||||
SELECT '1900000000000000552', '1900000000000000550', '编辑', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_warehouse_area:edit', '1', 2.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000552');
|
||||
|
||||
INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`)
|
||||
SELECT '1900000000000000553', '1900000000000000550', '删除', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_warehouse_area:delete', '1', 3.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000553');
|
||||
|
||||
INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`)
|
||||
SELECT '1900000000000000554', '1900000000000000550', '批量删除', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_warehouse_area:deleteBatch', '1', 4.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000554');
|
||||
|
||||
INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`)
|
||||
SELECT '1900000000000000555', '1900000000000000550', '导出', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_warehouse_area:exportXls', '1', 5.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000555');
|
||||
|
||||
INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`)
|
||||
SELECT '1900000000000000556', '1900000000000000550', '导入', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_warehouse_area:importExcel', '1', 6.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000556');
|
||||
|
||||
INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`)
|
||||
SELECT '1900000000000000557', '1900000000000000550', '启用/停用', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_warehouse_area:updateStatus', '1', 7.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000557');
|
||||
|
||||
-- ===================== 3. 角色菜单授权(admin 角色)=====================
|
||||
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
|
||||
SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000550', NULL, NOW(), '127.0.0.1'
|
||||
FROM `sys_role` r WHERE r.`role_code` = 'admin'
|
||||
AND NOT EXISTS (SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.id AND rp.`permission_id` = '1900000000000000550');
|
||||
|
||||
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
|
||||
SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000551', NULL, NOW(), '127.0.0.1'
|
||||
FROM `sys_role` r WHERE r.`role_code` = 'admin'
|
||||
AND NOT EXISTS (SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.id AND rp.`permission_id` = '1900000000000000551');
|
||||
|
||||
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
|
||||
SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000552', NULL, NOW(), '127.0.0.1'
|
||||
FROM `sys_role` r WHERE r.`role_code` = 'admin'
|
||||
AND NOT EXISTS (SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.id AND rp.`permission_id` = '1900000000000000552');
|
||||
|
||||
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
|
||||
SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000553', NULL, NOW(), '127.0.0.1'
|
||||
FROM `sys_role` r WHERE r.`role_code` = 'admin'
|
||||
AND NOT EXISTS (SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.id AND rp.`permission_id` = '1900000000000000553');
|
||||
|
||||
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
|
||||
SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000554', NULL, NOW(), '127.0.0.1'
|
||||
FROM `sys_role` r WHERE r.`role_code` = 'admin'
|
||||
AND NOT EXISTS (SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.id AND rp.`permission_id` = '1900000000000000554');
|
||||
|
||||
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
|
||||
SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000555', NULL, NOW(), '127.0.0.1'
|
||||
FROM `sys_role` r WHERE r.`role_code` = 'admin'
|
||||
AND NOT EXISTS (SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.id AND rp.`permission_id` = '1900000000000000555');
|
||||
|
||||
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
|
||||
SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000556', NULL, NOW(), '127.0.0.1'
|
||||
FROM `sys_role` r WHERE r.`role_code` = 'admin'
|
||||
AND NOT EXISTS (SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.id AND rp.`permission_id` = '1900000000000000556');
|
||||
|
||||
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
|
||||
SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000557', NULL, NOW(), '127.0.0.1'
|
||||
FROM `sys_role` r WHERE r.`role_code` = 'admin'
|
||||
AND NOT EXISTS (SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.id AND rp.`permission_id` = '1900000000000000557');
|
||||
@@ -0,0 +1,22 @@
|
||||
-- ============================================================
|
||||
-- 原料入场记录表:新增「拆码明细库位」拼接字段 portion_warehouse_locations
|
||||
-- 背景:桌面端「拆码明细」每行均可独立选择库位,需要把多行的库位
|
||||
-- 与「总份数 / 每份总重(KG) / 每份包数」一样以 "/" 分隔拼接持久化,
|
||||
-- 便于生成原材料卡片之后再次打开入场记录时正确回填每行库位。
|
||||
-- 字段:portion_warehouse_locations VARCHAR(500) DEFAULT NULL
|
||||
-- 形如:"1F-A01/1F-A02/2F-B05/"
|
||||
-- 注意:原 warehouse_location 字段保留,作为「基础资料」区的整票级单值。
|
||||
-- 幂等:列已存在则跳过 ADD。
|
||||
-- ============================================================
|
||||
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'mes_xsl_raw_material_entry'
|
||||
AND COLUMN_NAME = 'portion_warehouse_locations'
|
||||
);
|
||||
SET @ddl := IF(@col_exists > 0,
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `mes_xsl_raw_material_entry` ADD COLUMN `portion_warehouse_locations` varchar(500) DEFAULT NULL COMMENT ''拆码明细库位拼接(以 / 分隔,末尾带 /,如 1F-A01/1F-A02/)'' AFTER `warehouse_location`'
|
||||
);
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
@@ -0,0 +1,49 @@
|
||||
-- ============================================================
|
||||
-- 拆码明细 ID 关联:
|
||||
-- 1) mes_xsl_raw_material_card.split_detail_id VARCHAR(64) — 关联到拆码明细行(每行一个 GUID)
|
||||
-- 2) mes_xsl_raw_material_entry.portion_detail_ids VARCHAR(800) — 拆码明细每行的 GUID 拼接(以 / 分隔,末尾带 /)
|
||||
--
|
||||
-- 用途:
|
||||
-- - 「生成原材料卡片」时把当前明细行的 ID 写入卡片 split_detail_id;
|
||||
-- - 「重新拆码」时按入场记录的 portion_detail_ids 反查并清除所有关联卡片。
|
||||
-- 幂等:列已存在则跳过 ADD。
|
||||
-- ============================================================
|
||||
|
||||
-- 1) mes_xsl_raw_material_card.split_detail_id
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'mes_xsl_raw_material_card'
|
||||
AND COLUMN_NAME = 'split_detail_id'
|
||||
);
|
||||
SET @ddl := IF(@col_exists > 0,
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `mes_xsl_raw_material_card` ADD COLUMN `split_detail_id` varchar(64) DEFAULT NULL COMMENT ''关联的拆码明细行 ID(GUID,可空,便于按入场记录批删)'' AFTER `barcode`'
|
||||
);
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
-- 1.1) 为 split_detail_id 加普通索引(重新拆码批删按此字段 IN 过滤)
|
||||
SET @idx_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'mes_xsl_raw_material_card'
|
||||
AND INDEX_NAME = 'idx_card_split_detail_id'
|
||||
);
|
||||
SET @ddl := IF(@idx_exists > 0,
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `mes_xsl_raw_material_card` ADD INDEX `idx_card_split_detail_id` (`split_detail_id`)'
|
||||
);
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
-- 2) mes_xsl_raw_material_entry.portion_detail_ids
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'mes_xsl_raw_material_entry'
|
||||
AND COLUMN_NAME = 'portion_detail_ids'
|
||||
);
|
||||
SET @ddl := IF(@col_exists > 0,
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `mes_xsl_raw_material_entry` ADD COLUMN `portion_detail_ids` varchar(800) DEFAULT NULL COMMENT ''拆码明细每行的 GUID 拼接(以 / 分隔,末尾带 /,与 total_portions 等同行对齐)'' AFTER `portion_warehouse_locations`'
|
||||
);
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
@@ -0,0 +1,24 @@
|
||||
-- ============================================================
|
||||
-- 原料入场记录表:新增「行级 已生成卡片 标志」拼接字段 portion_card_flags
|
||||
-- 背景:之前桌面端用 entry.print_flag(整票) 推断每行的 HasCard,
|
||||
-- 导致已打印记录上新增的明细行(保存后未生成卡片)也被误判为「已打印」,
|
||||
-- 且会绕过「未打印才允许生成」的过滤,造成重复加卡风险。
|
||||
-- 修复:把每行的 HasCard 真实状态按 / 分隔拼接持久化,
|
||||
-- 桌面端「生成原材料卡片」前以本字段为唯一依据过滤。
|
||||
-- 字段:portion_card_flags VARCHAR(500) DEFAULT NULL
|
||||
-- 形如:"1/1/0/" 表示该入场记录 3 条明细中前 2 行已生成卡片、第 3 行未生成。
|
||||
-- 历史数据:留空时桌面端降级用 print_flag 推断,与原行为兼容。
|
||||
-- 幂等:列已存在则跳过 ADD。
|
||||
-- ============================================================
|
||||
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'mes_xsl_raw_material_entry'
|
||||
AND COLUMN_NAME = 'portion_card_flags'
|
||||
);
|
||||
SET @ddl := IF(@col_exists > 0,
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `mes_xsl_raw_material_entry` ADD COLUMN `portion_card_flags` varchar(500) DEFAULT NULL COMMENT ''拆码明细行级 已生成卡片 标志拼接(以 / 分隔,1=已生成 0=未生成,末尾带 /,如 1/1/0/)'' AFTER `portion_warehouse_locations`'
|
||||
);
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
@@ -0,0 +1,23 @@
|
||||
-- 原料入场记录:新增「结存入库」按钮权限(parent_id=1900000000000000530,sort_no=7)
|
||||
|
||||
INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES
|
||||
('1900000000000000537', '1900000000000000530', '结存入库', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_raw_material_entry:stockIn', '1', 7.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`parent_id` = VALUES(`parent_id`),
|
||||
`name` = VALUES(`name`),
|
||||
`menu_type` = VALUES(`menu_type`),
|
||||
`perms` = VALUES(`perms`),
|
||||
`perms_type` = VALUES(`perms_type`),
|
||||
`sort_no` = VALUES(`sort_no`),
|
||||
`del_flag` = VALUES(`del_flag`),
|
||||
`status` = VALUES(`status`),
|
||||
`update_by` = VALUES(`update_by`),
|
||||
`update_time` = VALUES(`update_time`);
|
||||
|
||||
-- 默认管理员角色授权
|
||||
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
|
||||
SELECT REPLACE(UUID(), '-', ''), 'f6817f48af4fb3af11b9e8bf182f618b', '1900000000000000537', NULL, NOW(), '127.0.0.1'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `sys_role_permission`
|
||||
WHERE `role_id` = 'f6817f48af4fb3af11b9e8bf182f618b' AND `permission_id` = '1900000000000000537'
|
||||
);
|
||||
@@ -63,4 +63,14 @@ export const formSchema: FormSchema[] = [
|
||||
required: true,
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
label: '分类编码',
|
||||
field: 'code',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
// 新增时:留空则后端按 FillRule 自动生成(如 A01.A02);
|
||||
// 编辑时:保留原值,避免缺字段提交导致编码被清空。
|
||||
placeholder: '留空将按规则自动生成(如 A01.A02)',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -7,13 +7,13 @@ export const columns: BasicColumn[] = [
|
||||
title: '条码',
|
||||
align: 'center',
|
||||
dataIndex: 'barcode',
|
||||
width: 160,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '批次号',
|
||||
align: 'center',
|
||||
dataIndex: 'batchNo',
|
||||
width: 160,
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '入场日期',
|
||||
|
||||
@@ -123,12 +123,14 @@
|
||||
}
|
||||
|
||||
async function handlePriorityChange(record, checked: boolean) {
|
||||
const newVal = checked ? '1' : '0';
|
||||
const oldVal = record.priorityPickup;
|
||||
record.priorityPickup = newVal;
|
||||
try {
|
||||
await updatePriority(record.id, checked ? '1' : '0');
|
||||
record.priorityPickup = checked ? '1' : '0';
|
||||
createMessage.success('优先出库设置已更新');
|
||||
await updatePriority(record.id, newVal);
|
||||
} catch {
|
||||
createMessage.error('更新失败,请重试');
|
||||
record.priorityPickup = oldVal;
|
||||
createMessage.error('优先出库更新失败,请重试');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ enum Api {
|
||||
edit = '/xslmes/mesXslRawMaterialEntry/edit',
|
||||
deleteOne = '/xslmes/mesXslRawMaterialEntry/delete',
|
||||
deleteBatch = '/xslmes/mesXslRawMaterialEntry/deleteBatch',
|
||||
batchStockIn = '/xslmes/mesXslRawMaterialEntry/batchStockIn',
|
||||
importExcel = '/xslmes/mesXslRawMaterialEntry/importExcel',
|
||||
exportXls = '/xslmes/mesXslRawMaterialEntry/exportXls',
|
||||
}
|
||||
@@ -39,6 +40,9 @@ export const batchDelete = (params, handleSuccess) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const batchStockIn = (ids: string) =>
|
||||
defHttp.put({ url: Api.batchStockIn, params: { ids } }, { joinParamsToUrl: true });
|
||||
|
||||
export const saveOrUpdate = (params, isUpdate) => {
|
||||
let url = isUpdate ? Api.edit : Api.save;
|
||||
return defHttp.post({ url: url, params });
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { BasicColumn, FormSchema } from '/@/components/Table';
|
||||
|
||||
export const columns: BasicColumn[] = [
|
||||
{ title: '条码', align: 'center', dataIndex: 'barcode', width: 160 },
|
||||
{ title: '批次号', align: 'center', dataIndex: 'batchNo', width: 140 },
|
||||
{ title: '条码', align: 'center', dataIndex: 'barcode', width: 200 },
|
||||
{ title: '批次号', align: 'center', dataIndex: 'batchNo', width: 180 },
|
||||
{
|
||||
title: '入场时间',
|
||||
align: 'center',
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<a-button type="primary" v-auth="'xslmes:mes_xsl_raw_material_entry:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
|
||||
<a-button type="primary" v-auth="'xslmes:mes_xsl_raw_material_entry:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
|
||||
<j-upload-button type="primary" v-auth="'xslmes:mes_xsl_raw_material_entry:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
|
||||
<a-button type="primary" v-auth="'xslmes:mes_xsl_raw_material_entry:stockIn'" preIcon="ant-design:check-circle-outlined" @click="handleStockIn"> 结存入库</a-button>
|
||||
<a-dropdown v-if="selectedRowKeys.length > 0">
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
@@ -30,13 +31,15 @@
|
||||
|
||||
<script lang="ts" name="xslmes-mesXslRawMaterialEntry" setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { BasicTable, useTable, TableAction } from '/@/components/Table';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
import MesXslRawMaterialEntryModal from './components/MesXslRawMaterialEntryModal.vue';
|
||||
import { columns, searchFormSchema, superQuerySchema } from './MesXslRawMaterialEntry.data';
|
||||
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl } from './MesXslRawMaterialEntry.api';
|
||||
import { list, deleteOne, batchDelete, batchStockIn, getImportUrl, getExportUrl } from './MesXslRawMaterialEntry.api';
|
||||
|
||||
const { createConfirm, createMessage } = useMessage();
|
||||
const queryParam = reactive<any>({});
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
|
||||
@@ -55,7 +58,7 @@
|
||||
],
|
||||
},
|
||||
actionColumn: {
|
||||
width: 120,
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
},
|
||||
beforeFetch: (params) => {
|
||||
@@ -103,6 +106,25 @@
|
||||
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
|
||||
}
|
||||
|
||||
function handleStockIn() {
|
||||
if (!selectedRowKeys.value.length) {
|
||||
createMessage.warning('请先勾选要结存入库的记录');
|
||||
return;
|
||||
}
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: '结存入库',
|
||||
content: `是否将选中的 ${selectedRowKeys.value.length} 条记录结存入库`,
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
await batchStockIn(selectedRowKeys.value.join(','));
|
||||
createMessage.success('结存入库成功!');
|
||||
handleSuccess();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
(selectedRowKeys.value = []) && reload();
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
</div>
|
||||
<MesXslWarehouseModal @register="registerModal" @success="handleSuccess" />
|
||||
<MesXslWarehouseSysCategoryModal @register="registerCategoryModal" @success="onCategorySuccess" />
|
||||
<MesXslWarehouseAreaBatchAddModal @register="registerBatchAreaModal" @success="handleSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -106,6 +107,7 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import MesXslWarehouseModal from './components/MesXslWarehouseModal.vue';
|
||||
import MesXslWarehouseSysCategoryModal from './components/MesXslWarehouseSysCategoryModal.vue';
|
||||
import MesXslWarehouseAreaBatchAddModal from './components/MesXslWarehouseAreaBatchAddModal.vue';
|
||||
import { columns, searchFormSchema, superQuerySchema, WH_CATEGORY_CUSTOMER_CODE, WH_CATEGORY_SUPPLIER_CODE } from './MesXslWarehouse.data';
|
||||
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl, updateStatus } from './MesXslWarehouse.api';
|
||||
import { loadTreeData as loadCategoryTreeRoot } from '/@/views/system/category/category.api';
|
||||
@@ -310,6 +312,7 @@
|
||||
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
const [registerCategoryModal, { openModal: openCategoryModal }] = useModal();
|
||||
const [registerBatchAreaModal, { openModal: openBatchAreaModal }] = useModal();
|
||||
|
||||
function handleAddCategory() {
|
||||
const rootId = warehouseCategoryRootId.value;
|
||||
@@ -507,8 +510,15 @@
|
||||
];
|
||||
}
|
||||
|
||||
function handleBatchAddArea(record: Recordable) {
|
||||
openBatchAreaModal(true, { record });
|
||||
}
|
||||
|
||||
function getDropDownAction(record) {
|
||||
return [{ label: '详情', onClick: handleDetail.bind(null, record) }];
|
||||
return [
|
||||
{ label: '详情', onClick: handleDetail.bind(null, record) },
|
||||
{ label: '批量添加库区', onClick: handleBatchAddArea.bind(null, record) },
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
v-bind="$attrs"
|
||||
@register="registerModal"
|
||||
destroyOnClose
|
||||
:title="title"
|
||||
:width="700"
|
||||
:confirmLoading="submitting"
|
||||
@ok="handleSubmit"
|
||||
>
|
||||
<div style="margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center">
|
||||
<span style="color: #666; font-size: 13px">共 {{ rows.length }} 条</span>
|
||||
<a-button type="primary" size="small" preIcon="ant-design:plus-outlined" @click="addRow">新增行</a-button>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="tableColumns"
|
||||
:data-source="rows"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
bordered
|
||||
row-key="__key"
|
||||
:scroll="{ y: 380 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'seq'">
|
||||
{{ index + 1 }}
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'areaCode'">
|
||||
<a-input
|
||||
v-model:value="record.areaCode"
|
||||
placeholder="库区编码(必填)"
|
||||
size="small"
|
||||
:status="record.__codeError ? 'error' : ''"
|
||||
@change="onAreaCodeChange(record)"
|
||||
@blur="validateCode(record)"
|
||||
/>
|
||||
<div v-if="record.__codeError" style="color: #ff4d4f; font-size: 12px; margin-top: 2px">
|
||||
{{ record.__codeError }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'areaName'">
|
||||
<a-input v-model:value="record.areaName" placeholder="默认同库区编码" size="small" />
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'maxCapacity'">
|
||||
<a-input-number
|
||||
v-model:value="record.maxCapacity"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
placeholder="最大存放量"
|
||||
size="small"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-button danger type="link" size="small" @click="removeRow(index)">删除</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<div v-if="rows.length === 0" style="text-align: center; color: #999; padding: 24px 0; font-size: 13px">
|
||||
暂无库区明细,点击「新增行」添加
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { batchAddAreas } from '/@/views/xslmes/mesXslWarehouseArea/MesXslWarehouseArea.api';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const emit = defineEmits(['register', 'success']);
|
||||
|
||||
const warehouseId = ref('');
|
||||
const warehouseName = ref('');
|
||||
const warehouseCategory = ref<string | undefined>(undefined);
|
||||
const tenantId = ref<number | undefined>(undefined);
|
||||
const submitting = ref(false);
|
||||
|
||||
let _rowKey = 0;
|
||||
|
||||
interface AreaRow {
|
||||
__key: number;
|
||||
__codeError: string;
|
||||
__prevCode: string;
|
||||
areaCode: string;
|
||||
areaName: string;
|
||||
maxCapacity: number | undefined;
|
||||
}
|
||||
|
||||
const rows = ref<AreaRow[]>([]);
|
||||
|
||||
const title = computed(() =>
|
||||
warehouseName.value ? `批量添加库区 — ${warehouseName.value}` : '批量添加库区'
|
||||
);
|
||||
|
||||
const tableColumns = [
|
||||
{ title: '序号', key: 'seq', width: 52, align: 'center' },
|
||||
{ title: '库区编码 *', key: 'areaCode', width: 180 },
|
||||
{ title: '库区名称', key: 'areaName', width: 180 },
|
||||
{ title: '最大存放量', key: 'maxCapacity', width: 130 },
|
||||
{ title: '操作', key: 'action', width: 60, align: 'center' },
|
||||
];
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner((data) => {
|
||||
rows.value = [];
|
||||
_rowKey = 0;
|
||||
warehouseId.value = data?.record?.id ?? '';
|
||||
warehouseName.value = data?.record?.warehouseName ?? '';
|
||||
warehouseCategory.value = data?.record?.warehouseCategory ?? undefined;
|
||||
tenantId.value = data?.record?.tenantId ?? undefined;
|
||||
addRow();
|
||||
});
|
||||
|
||||
function addRow() {
|
||||
rows.value.push({
|
||||
__key: _rowKey++,
|
||||
__codeError: '',
|
||||
__prevCode: '',
|
||||
areaCode: '',
|
||||
areaName: '',
|
||||
maxCapacity: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function removeRow(index: number) {
|
||||
rows.value.splice(index, 1);
|
||||
}
|
||||
|
||||
function onAreaCodeChange(record: AreaRow) {
|
||||
if (!record.areaName || record.areaName === record.__prevCode) {
|
||||
record.areaName = record.areaCode;
|
||||
}
|
||||
record.__prevCode = record.areaCode;
|
||||
record.__codeError = '';
|
||||
}
|
||||
|
||||
function validateCode(record: AreaRow) {
|
||||
if (!record.areaCode || !record.areaCode.trim()) {
|
||||
record.__codeError = '库区编码不能为空';
|
||||
} else {
|
||||
record.__codeError = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (rows.value.length === 0) {
|
||||
createMessage.warning('请至少添加一条库区记录');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate all rows
|
||||
let hasError = false;
|
||||
const codeSet = new Set<string>();
|
||||
for (const row of rows.value) {
|
||||
const code = (row.areaCode || '').trim();
|
||||
if (!code) {
|
||||
row.__codeError = '库区编码不能为空';
|
||||
hasError = true;
|
||||
continue;
|
||||
}
|
||||
if (codeSet.has(code)) {
|
||||
row.__codeError = '库区编码重复';
|
||||
hasError = true;
|
||||
continue;
|
||||
}
|
||||
codeSet.add(code);
|
||||
row.__codeError = '';
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
createMessage.error('请修正红色标记的错误后再提交');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = rows.value.map((row) => ({
|
||||
areaCode: row.areaCode.trim(),
|
||||
areaName: (row.areaName || row.areaCode).trim() || row.areaCode.trim(),
|
||||
maxCapacity: row.maxCapacity ?? undefined,
|
||||
warehouseId: warehouseId.value,
|
||||
warehouseName: warehouseName.value,
|
||||
warehouseCategory: warehouseCategory.value,
|
||||
tenantId: tenantId.value,
|
||||
status: '0',
|
||||
}));
|
||||
|
||||
try {
|
||||
submitting.value = true;
|
||||
setModalProps({ confirmLoading: true });
|
||||
await batchAddAreas(payload);
|
||||
createMessage.success(`批量添加成功,共创建 ${payload.length} 个库区!`);
|
||||
closeModal();
|
||||
emit('success');
|
||||
} catch {
|
||||
// error handled by global interceptor
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,71 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
const { createConfirm } = useMessage();
|
||||
|
||||
enum Api {
|
||||
list = '/xslmes/mesXslWarehouseArea/list',
|
||||
checkAreaCode = '/xslmes/mesXslWarehouseArea/checkAreaCode',
|
||||
queryById = '/xslmes/mesXslWarehouseArea/queryById',
|
||||
save = '/xslmes/mesXslWarehouseArea/add',
|
||||
edit = '/xslmes/mesXslWarehouseArea/edit',
|
||||
updateStatus = '/xslmes/mesXslWarehouseArea/updateStatus',
|
||||
deleteOne = '/xslmes/mesXslWarehouseArea/delete',
|
||||
deleteBatch = '/xslmes/mesXslWarehouseArea/deleteBatch',
|
||||
importExcel = '/xslmes/mesXslWarehouseArea/importExcel',
|
||||
exportXls = '/xslmes/mesXslWarehouseArea/exportXls',
|
||||
batchAdd = '/xslmes/mesXslWarehouseArea/batchAdd',
|
||||
}
|
||||
|
||||
export const getExportUrl = Api.exportXls;
|
||||
export const getImportUrl = Api.importExcel;
|
||||
|
||||
export const list = (params) => defHttp.get({ url: Api.list, params });
|
||||
|
||||
export const queryById = (params: { id: string }) => defHttp.get({ url: Api.queryById, params });
|
||||
|
||||
/** 库区编码唯一性校验(同租户;编辑传 dataId) */
|
||||
export const checkAreaCode = (params: { areaCode: string; dataId?: string }) =>
|
||||
defHttp.get(
|
||||
{ url: Api.checkAreaCode, params },
|
||||
{
|
||||
successMessageMode: 'none',
|
||||
errorMessageMode: 'none',
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteOne = (params, handleSuccess) => {
|
||||
return defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
};
|
||||
|
||||
export const batchDelete = (params, handleSuccess) => {
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: '确认删除',
|
||||
content: '是否删除选中数据',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const saveOrUpdate = (params, isUpdate) => {
|
||||
const url = isUpdate ? Api.edit : Api.save;
|
||||
return defHttp.post({ url, params });
|
||||
};
|
||||
|
||||
/** 批量添加库区(同一仓库下一次性创建多条) */
|
||||
export const batchAddAreas = (params: any[]) => defHttp.post({ url: Api.batchAdd, params });
|
||||
|
||||
/** 启用/停用:status 0 启用 1 停用(字典 xslmes_unit_status) */
|
||||
export const updateStatus = (params: { id: string; status: string }, handleSuccess?: () => void) => {
|
||||
return defHttp.post({ url: Api.updateStatus, params }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess?.();
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
import { BasicColumn, FormSchema } from '/@/components/Table';
|
||||
import { checkAreaCode } from './MesXslWarehouseArea.api';
|
||||
|
||||
export const columns: BasicColumn[] = [
|
||||
{ title: 'ID', align: 'center', dataIndex: 'id', width: 280, ellipsis: true, defaultHidden: true },
|
||||
{ title: '库区编码', align: 'center', dataIndex: 'areaCode', width: 120 },
|
||||
{ title: '库区名称', align: 'center', dataIndex: 'areaName', width: 160 },
|
||||
{ title: '所属仓库', align: 'center', dataIndex: 'warehouseName', width: 160 },
|
||||
{ title: '仓库分类', align: 'center', dataIndex: 'warehouseCategory_dictText', width: 140 },
|
||||
{ title: '最大存放量', align: 'center', dataIndex: 'maxCapacity', width: 110 },
|
||||
{ title: '实际存放量', align: 'center', dataIndex: 'actualCapacity', width: 110 },
|
||||
{ title: '状态', align: 'center', dataIndex: 'status_dictText', width: 90 },
|
||||
{ title: '备注', align: 'center', dataIndex: 'remark', width: 200, ellipsis: true },
|
||||
{ title: '创建人', align: 'center', dataIndex: 'createBy', width: 100 },
|
||||
{
|
||||
title: '创建时间',
|
||||
align: 'center',
|
||||
dataIndex: 'createTime',
|
||||
width: 165,
|
||||
customRender: ({ text }) => (!text ? '' : String(text).length > 19 ? String(text).substring(0, 19) : text),
|
||||
},
|
||||
{ title: '租户ID', align: 'center', dataIndex: 'tenantId', width: 90, defaultHidden: true },
|
||||
];
|
||||
|
||||
export const searchFormSchema: FormSchema[] = [
|
||||
{ label: '库区编码', field: 'areaCode', component: 'JInput', colProps: { span: 6 } },
|
||||
{ label: '库区名称', field: 'areaName', component: 'JInput', colProps: { span: 6 } },
|
||||
{
|
||||
label: '所属仓库',
|
||||
field: 'warehouseId',
|
||||
component: 'JSearchSelect',
|
||||
componentProps: { dict: 'mes_xsl_warehouse,warehouse_name,id', async: true, placeholder: '请搜索仓库' },
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
field: 'status',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: { dictCode: 'xslmes_unit_status' },
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
];
|
||||
|
||||
export const formSchema: FormSchema[] = [
|
||||
{ label: '', field: 'id', component: 'Input', show: false },
|
||||
{ label: '', field: 'warehouseName', component: 'Input', show: false },
|
||||
{
|
||||
label: '库区编码',
|
||||
field: 'areaCode',
|
||||
required: true,
|
||||
component: 'Input',
|
||||
componentProps: { placeholder: '同租户内唯一,不可与已有库区重复' },
|
||||
dynamicRules: ({ model }) => [
|
||||
{ required: true, message: '请输入库区编码' },
|
||||
{
|
||||
validator: async (_rule, value) => {
|
||||
const v = value == null ? '' : String(value).trim();
|
||||
if (!v) return Promise.resolve();
|
||||
try {
|
||||
await checkAreaCode({ areaCode: v, dataId: model?.id });
|
||||
return Promise.resolve();
|
||||
} catch (e: any) {
|
||||
const msg = e?.response?.data?.message || e?.message || '该库区编码已存在';
|
||||
return Promise.reject(msg);
|
||||
}
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '库区名称',
|
||||
field: 'areaName',
|
||||
required: true,
|
||||
component: 'Input',
|
||||
componentProps: { placeholder: '请输入库区名称' },
|
||||
},
|
||||
{
|
||||
label: '所属仓库',
|
||||
field: 'warehouseId',
|
||||
required: true,
|
||||
component: 'Input',
|
||||
slot: 'warehousePicker',
|
||||
},
|
||||
{
|
||||
label: '仓库分类',
|
||||
field: 'warehouseCategory',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: { dictCode: 'sys_category,name,id', disabled: true, placeholder: '选择仓库后自动带出' },
|
||||
},
|
||||
{
|
||||
label: '最大存放量',
|
||||
field: 'maxCapacity',
|
||||
component: 'InputNumber',
|
||||
componentProps: { placeholder: '请输入最大存放量', style: { width: '100%' }, min: 0 },
|
||||
},
|
||||
{
|
||||
label: '实际存放量',
|
||||
field: 'actualCapacity',
|
||||
component: 'InputNumber',
|
||||
componentProps: { placeholder: '请输入实际存放量', style: { width: '100%' }, min: 0 },
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
field: 'remark',
|
||||
component: 'InputTextArea',
|
||||
componentProps: { placeholder: '请输入备注', rows: 3 },
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
field: 'status',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: { dictCode: 'xslmes_unit_status', placeholder: '请选择状态' },
|
||||
},
|
||||
{
|
||||
label: '租户ID',
|
||||
field: 'tenantId',
|
||||
component: 'InputNumber',
|
||||
componentProps: { placeholder: '租户ID,可空', style: { width: '100%' } },
|
||||
},
|
||||
];
|
||||
|
||||
export const superQuerySchema = {
|
||||
areaCode: { title: '库区编码', order: 0, view: 'text' },
|
||||
areaName: { title: '库区名称', order: 1, view: 'text' },
|
||||
warehouseName: { title: '所属仓库', order: 2, view: 'text' },
|
||||
warehouseCategory: { title: '仓库分类', order: 3, view: 'list', dictCode: 'sys_category,name,id' },
|
||||
status: { title: '状态', order: 4, view: 'list', dictCode: 'xslmes_unit_status' },
|
||||
maxCapacity: { title: '最大存放量', order: 5, view: 'number' },
|
||||
actualCapacity: { title: '实际存放量', order: 6, view: 'number' },
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div>
|
||||
<BasicTable @register="registerTable" :rowSelection="rowSelection">
|
||||
<template #tableTitle>
|
||||
<a-button type="primary" v-auth="'xslmes:mes_xsl_warehouse_area:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
|
||||
<a-button type="primary" v-auth="'xslmes:mes_xsl_warehouse_area:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
|
||||
<j-upload-button type="primary" v-auth="'xslmes:mes_xsl_warehouse_area:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
|
||||
<a-dropdown v-if="selectedRowKeys.length > 0">
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="1" @click="batchHandleDelete">
|
||||
<Icon icon="ant-design:delete-outlined" />
|
||||
删除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button v-auth="'xslmes:mes_xsl_warehouse_area:deleteBatch'">
|
||||
批量操作
|
||||
<Icon icon="mdi:chevron-down" />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
|
||||
</template>
|
||||
</BasicTable>
|
||||
<MesXslWarehouseAreaModal @register="registerModal" @success="handleSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="xslmes-mesXslWarehouseArea" setup>
|
||||
import { reactive } from 'vue';
|
||||
import { BasicTable, TableAction } from '/@/components/Table';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
import MesXslWarehouseAreaModal from './components/MesXslWarehouseAreaModal.vue';
|
||||
import { columns, searchFormSchema, superQuerySchema } from './MesXslWarehouseArea.data';
|
||||
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl, updateStatus } from './MesXslWarehouseArea.api';
|
||||
import Icon from '/@/components/Icon';
|
||||
import type { Recordable } from '/@/types/global';
|
||||
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
|
||||
const { tableContext, onExportXls, onImportXls } = useListPage({
|
||||
tableProps: {
|
||||
title: '库区管理',
|
||||
api: list,
|
||||
columns,
|
||||
canResize: true,
|
||||
formConfig: {
|
||||
schemas: searchFormSchema,
|
||||
autoSubmitOnEnter: true,
|
||||
showAdvancedButton: true,
|
||||
},
|
||||
actionColumn: {
|
||||
width: 260,
|
||||
fixed: 'right',
|
||||
},
|
||||
},
|
||||
exportConfig: {
|
||||
name: '库区管理',
|
||||
url: getExportUrl,
|
||||
},
|
||||
importConfig: {
|
||||
url: getImportUrl,
|
||||
success: handleSuccess,
|
||||
},
|
||||
});
|
||||
|
||||
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
|
||||
const superQueryConfig = reactive(superQuerySchema);
|
||||
|
||||
function handleAdd() {
|
||||
openModal(true, { isUpdate: false, showFooter: true, record: {} });
|
||||
}
|
||||
|
||||
function handleEdit(record: Recordable) {
|
||||
openModal(true, { record, isUpdate: true, showFooter: true });
|
||||
}
|
||||
|
||||
function handleDetail(record: Recordable) {
|
||||
openModal(true, { record, isUpdate: true, showFooter: false });
|
||||
}
|
||||
|
||||
async function handleDelete(record) {
|
||||
await deleteOne({ id: record.id }, handleSuccess);
|
||||
}
|
||||
|
||||
function isRecordEnabled(record: Recordable) {
|
||||
return record.status === '0' || record.status === 0;
|
||||
}
|
||||
|
||||
async function handleToggleStatus(record: Recordable, status: string) {
|
||||
await updateStatus({ id: record.id, status }, handleSuccess);
|
||||
}
|
||||
|
||||
async function batchHandleDelete() {
|
||||
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
selectedRowKeys.value = [];
|
||||
reload();
|
||||
}
|
||||
|
||||
function handleSuperQuery(params) {
|
||||
Object.assign({}, params);
|
||||
reload();
|
||||
}
|
||||
|
||||
function getTableAction(record) {
|
||||
const enabled = isRecordEnabled(record);
|
||||
return [
|
||||
{
|
||||
label: '编辑',
|
||||
onClick: handleEdit.bind(null, record),
|
||||
auth: 'xslmes:mes_xsl_warehouse_area:edit',
|
||||
},
|
||||
{
|
||||
label: '启用',
|
||||
ifShow: !enabled,
|
||||
onClick: handleToggleStatus.bind(null, record, '0'),
|
||||
auth: 'xslmes:mes_xsl_warehouse_area:updateStatus',
|
||||
},
|
||||
{
|
||||
label: '停用',
|
||||
ifShow: enabled,
|
||||
onClick: handleToggleStatus.bind(null, record, '1'),
|
||||
auth: 'xslmes:mes_xsl_warehouse_area:updateStatus',
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
popConfirm: {
|
||||
title: '是否确认删除',
|
||||
confirm: handleDelete.bind(null, record),
|
||||
},
|
||||
auth: 'xslmes:mes_xsl_warehouse_area:delete',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getDropDownAction(record) {
|
||||
return [{ label: '详情', onClick: handleDetail.bind(null, record) }];
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="680" @ok="handleSubmit">
|
||||
<BasicForm @register="registerForm">
|
||||
<template #warehousePicker="{ model, field }">
|
||||
<JSearchSelect
|
||||
v-model:value="model[field]"
|
||||
dict="mes_xsl_warehouse,warehouse_name,id"
|
||||
:async="true"
|
||||
placeholder="请搜索并选择仓库"
|
||||
:disabled="isDetail"
|
||||
@change="(v) => onWarehouseChange(model, v)"
|
||||
/>
|
||||
</template>
|
||||
</BasicForm>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, unref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicForm, useForm, JSearchSelect } from '/@/components/Form';
|
||||
import { formSchema } from '../MesXslWarehouseArea.data';
|
||||
import { saveOrUpdate } from '../MesXslWarehouseArea.api';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
const emit = defineEmits(['register', 'success']);
|
||||
|
||||
const isUpdate = ref(true);
|
||||
const isDetail = ref(false);
|
||||
|
||||
const [registerForm, { setProps, resetFields, setFieldsValue, validate, scrollToField }] = useForm({
|
||||
labelWidth: 120,
|
||||
schemas: formSchema,
|
||||
showActionButtonGroup: false,
|
||||
baseColProps: { span: 24 },
|
||||
});
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
await resetFields();
|
||||
setModalProps({ confirmLoading: false, showCancelBtn: !!data?.showFooter, showOkBtn: !!data?.showFooter });
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
isDetail.value = !data?.showFooter;
|
||||
|
||||
if (unref(isUpdate)) {
|
||||
await setFieldsValue({ ...data.record });
|
||||
} else {
|
||||
await setFieldsValue({ status: '0' });
|
||||
}
|
||||
|
||||
setProps({ disabled: !data?.showFooter });
|
||||
});
|
||||
|
||||
const title = computed(() => (!unref(isUpdate) ? '新增库区' : unref(isDetail) ? '库区详情' : '编辑库区'));
|
||||
|
||||
async function onWarehouseChange(model: any, val: unknown) {
|
||||
const id = val != null && val !== '' ? String(val) : '';
|
||||
if (!id) {
|
||||
model.warehouseName = '';
|
||||
await setFieldsValue({ warehouseName: '', warehouseCategory: undefined });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const wh = await defHttp.get({ url: '/xslmes/mesXslWarehouse/queryById', params: { id } });
|
||||
if (wh) {
|
||||
await setFieldsValue({
|
||||
warehouseName: wh.warehouseName || '',
|
||||
warehouseCategory: wh.warehouseCategory || undefined,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const values = await validate();
|
||||
setModalProps({ confirmLoading: true });
|
||||
await saveOrUpdate(values, unref(isUpdate));
|
||||
closeModal();
|
||||
emit('success');
|
||||
} catch (e: any) {
|
||||
if (e?.errorFields) {
|
||||
const firstField = e.errorFields[0];
|
||||
if (firstField) {
|
||||
scrollToField(firstField.name, { behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}
|
||||
return Promise.reject(e);
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -18,6 +18,19 @@ export const columns: BasicColumn[] = [
|
||||
{ title: '毛重(KG)', align: 'center', dataIndex: 'grossWeight', width: 100 },
|
||||
{ title: '皮重(KG)', align: 'center', dataIndex: 'tareWeight', width: 100 },
|
||||
{ title: '净重(KG)', align: 'center', dataIndex: 'netWeight', width: 100 },
|
||||
{
|
||||
title: '已入场重量(KG)',
|
||||
align: 'center',
|
||||
dataIndex: 'enteredWeight',
|
||||
width: 120,
|
||||
customRender: ({ text }) => {
|
||||
// 后端实时计算返回,未匹配到入场记录时为 0
|
||||
if (text === null || text === undefined || text === '') return '0';
|
||||
const n = Number(text);
|
||||
if (Number.isNaN(n)) return String(text);
|
||||
return Number.isInteger(n) ? String(n) : n.toFixed(2);
|
||||
},
|
||||
},
|
||||
{ title: '司机', align: 'center', dataIndex: 'driverName', width: 90 },
|
||||
{ title: '手机号', align: 'center', dataIndex: 'driverPhone', width: 120 },
|
||||
];
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using Prism.Events;
|
||||
|
||||
namespace YY.Admin.Core.Events;
|
||||
|
||||
public class WarehouseAreaChangedPayload
|
||||
{
|
||||
public string Action { get; set; } = string.Empty;
|
||||
public string? WarehouseAreaId { get; set; }
|
||||
}
|
||||
|
||||
public class WarehouseAreaChangedEvent : PubSubEvent<WarehouseAreaChangedPayload>
|
||||
{
|
||||
}
|
||||
@@ -15,4 +15,12 @@ public interface IRawMaterialCardService
|
||||
Task<bool> DeleteAsync(string id, CancellationToken ct = default);
|
||||
Task<bool> DeleteBatchAsync(string ids, CancellationToken ct = default);
|
||||
Task<bool> UpdatePriorityAsync(string id, string priorityPickup, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// 按拆码明细 ID 列表批量删除原材料卡片。
|
||||
/// </summary>
|
||||
/// <param name="splitDetailIds">拆码明细行的 GUID 集合(自动 distinct,空跳过)</param>
|
||||
/// <param name="dryRun">为 true 时仅返回匹配数量、不真正删除(用于「重新拆码」弹窗预提示)</param>
|
||||
/// <returns>匹配/删除的卡片数量;失败返回 -1</returns>
|
||||
Task<int> DeleteBySplitDetailIdsAsync(IEnumerable<string> splitDetailIds, bool dryRun = false, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -23,4 +23,10 @@ public interface IRawMaterialEntryService
|
||||
|
||||
/// <summary>调用后端接口生成条码/批次号(格式:QH+物料编码+yyMMdd+序号)</summary>
|
||||
Task<string?> GenerateBarcodeAsync(string materialCode, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// 同步读取本地缓存的「全量入场记录」快照(深拷贝),不会触发远端拉取。
|
||||
/// 主要用于「磅单已入场重量」等跨表实时聚合,且需要保持与后端相同口径的场景。
|
||||
/// </summary>
|
||||
IReadOnlyList<MesXslRawMaterialEntry> GetCachedSnapshot();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
using YY.Admin.Core.Entity;
|
||||
|
||||
namespace YY.Admin.Core.Services;
|
||||
|
||||
public record WarehouseAreaPageResult(List<MesXslWarehouseArea> Records, long Total, int PageNo, int PageSize);
|
||||
|
||||
/// <summary>
|
||||
/// 库区管理服务(CRUD + 状态切换 + 编码校验)。
|
||||
/// 走 jeecg 免密接口 /xslmes/mesXslWarehouseArea/anon/*。
|
||||
///
|
||||
/// 注意:本接口契约不可随意改签名,被以下调用方引用:
|
||||
/// - WarehouseAreaListViewModel(列表/筛选/启停/删除)
|
||||
/// - WarehouseAreaEditDialogViewModel(新增/编辑/校验)
|
||||
/// - WarehouseAreaPickerDialogViewModel(拆码明细库位选择弹窗)
|
||||
/// </summary>
|
||||
public interface IWarehouseAreaService
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询。参数顺序与 WarehouseAreaListViewModel 调用一致:
|
||||
/// pageNo, pageSize, areaCode, areaName, warehouseId, status。
|
||||
/// </summary>
|
||||
/// <param name="status">"0" 启用 / "1" 停用 / "" 或 null 表示不限</param>
|
||||
Task<WarehouseAreaPageResult> PageAsync(
|
||||
int pageNo,
|
||||
int pageSize,
|
||||
string? areaCode = null,
|
||||
string? areaName = null,
|
||||
string? warehouseId = null,
|
||||
string? status = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>按 ID 查单条。</summary>
|
||||
Task<MesXslWarehouseArea?> GetByIdAsync(string id, CancellationToken ct = default);
|
||||
|
||||
/// <summary>新增库区。后端会校验 areaCode 重复。</summary>
|
||||
Task<bool> AddAsync(MesXslWarehouseArea area, CancellationToken ct = default);
|
||||
|
||||
/// <summary>编辑库区(按 ID)。</summary>
|
||||
Task<bool> EditAsync(MesXslWarehouseArea area, CancellationToken ct = default);
|
||||
|
||||
/// <summary>按 ID 删除。</summary>
|
||||
Task<bool> DeleteAsync(string id, CancellationToken ct = default);
|
||||
|
||||
/// <summary>切换启停状态:传入目标状态 "0"=启用 / "1"=停用。</summary>
|
||||
Task<bool> UpdateStatusAsync(string id, string newStatus, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// 校验库区编码是否可用(不存在则可用)。
|
||||
/// </summary>
|
||||
/// <param name="areaCode">待校验的库区编码</param>
|
||||
/// <param name="excludeId">编辑时排除自身 ID;新增传 null 或空</param>
|
||||
/// <returns>true=可用(无重复),false=已被占用</returns>
|
||||
Task<bool> CheckAreaCodeAsync(string areaCode, string? excludeId, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using YY.Admin.Core.Entity;
|
||||
|
||||
namespace YY.Admin.Core.Services;
|
||||
|
||||
public interface IWarehouseService
|
||||
{
|
||||
/// <summary>获取全部仓库列表(在线时拉取远端并刷新缓存,离线时返回本地缓存)</summary>
|
||||
Task<List<MesXslWarehouse>> GetAllAsync(CancellationToken ct = default);
|
||||
}
|
||||
@@ -4,6 +4,9 @@ public class MesXslRawMaterialCard
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? Barcode { get; set; }
|
||||
// 关联的拆码明细行 ID(GUID);「生成原材料卡片」时由桌面端填入,
|
||||
// 「重新拆码」按入场记录的 PortionDetailIds 批量 IN 删除关联卡片。
|
||||
public string? SplitDetailId { get; set; }
|
||||
public string? BatchNo { get; set; }
|
||||
public DateTime? EntryDate { get; set; }
|
||||
public string? MaterialId { get; set; }
|
||||
|
||||
@@ -23,6 +23,19 @@ public class MesXslRawMaterialEntry
|
||||
public string? TotalPortions { get; set; }
|
||||
public string? PortionWeight { get; set; }
|
||||
public string? PortionPackages { get; set; }
|
||||
// 拆码明细各行库位的拼接(以 / 分隔,末尾带 /,如 1F-A01/1F-A02/)。
|
||||
// 与 WarehouseLocation(基础资料整票级单值)独立,专供明细行回填。
|
||||
public string? PortionWarehouseLocations { get; set; }
|
||||
// 拆码明细每行的 GUID 拼接(以 / 分隔,末尾带 /),与其它 portion 字段行序对齐,
|
||||
// 用于「重新拆码」按拆码明细 ID 反查并清除关联原材料卡片。
|
||||
public string? PortionDetailIds { get; set; }
|
||||
/// <summary>
|
||||
/// 拆码明细行级「已生成卡片」标志拼接(以 / 分隔,末尾带 /,1=已生成 0=未生成)。
|
||||
/// 与 PortionDetailIds 行序对齐,作为「生成原材料卡片」过滤待生成行的唯一依据:
|
||||
/// HasCard==true 的行不再参与生成(避免重复加卡 + 条码冲突);HasCard==false 的行才参与续生成。
|
||||
/// 历史记录留空时桌面端降级用 PrintFlag 推断(持久化 ID 非空 && PrintFlag=1 ⇒ 视为已生成)。
|
||||
/// </summary>
|
||||
public string? PortionCardFlags { get; set; }
|
||||
|
||||
/// <summary>检测结果:0未检 1合格 2不合格</summary>
|
||||
public string? TestResult { get; set; }
|
||||
|
||||
11
yy-admin-master/YY.Admin.Core/Entity/MesXslWarehouse.cs
Normal file
11
yy-admin-master/YY.Admin.Core/Entity/MesXslWarehouse.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace YY.Admin.Core.Entity;
|
||||
|
||||
public class MesXslWarehouse
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? WarehouseCode { get; set; }
|
||||
public string? WarehouseName { get; set; }
|
||||
public string? WarehouseCategory { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public int? TenantId { get; set; }
|
||||
}
|
||||
31
yy-admin-master/YY.Admin.Core/Entity/MesXslWarehouseArea.cs
Normal file
31
yy-admin-master/YY.Admin.Core/Entity/MesXslWarehouseArea.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace YY.Admin.Core.Entity;
|
||||
|
||||
public class MesXslWarehouseArea
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? AreaCode { get; set; }
|
||||
public string? AreaName { get; set; }
|
||||
public string? WarehouseId { get; set; }
|
||||
public string? WarehouseName { get; set; }
|
||||
public string? WarehouseCategory { get; set; }
|
||||
|
||||
/// <summary>仓库分类名称(JeecgBoot @Dict 自动翻译,JSON key = warehouseCategory_dictText)</summary>
|
||||
[JsonPropertyName("warehouseCategory_dictText")]
|
||||
public string? WarehouseCategoryName { get; set; }
|
||||
public int? MaxCapacity { get; set; }
|
||||
public int? ActualCapacity { get; set; }
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>状态:0启用 1停用</summary>
|
||||
public string? Status { get; set; }
|
||||
|
||||
public int? TenantId { get; set; }
|
||||
public string? CreateBy { get; set; }
|
||||
public DateTime? CreateTime { get; set; }
|
||||
public string? UpdateBy { get; set; }
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
|
||||
public string StatusText => Status == "1" ? "停用" : "启用";
|
||||
}
|
||||
@@ -34,6 +34,12 @@ public class MesXslWeightRecord
|
||||
/// <summary>净重(KG)=毛重-皮重,自动计算</summary>
|
||||
public double? NetWeight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已入场重量(KG) —— 后端实时计算,不入库。
|
||||
/// 来源:所有引用本榜单(BillNo)的原料入场记录拆码明细的 (份数×每份重量) 累计求和。
|
||||
/// </summary>
|
||||
public double? EnteredWeight { get; set; }
|
||||
|
||||
/// <summary>司机姓名</summary>
|
||||
public string? DriverName { get; set; }
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
|
||||
new SysMenu{ Id=1300150010801, Pid=1300150000101, Title="新增原料入场记录", Path="/xslmes/rawMaterialEntryOperation", Name="rawMaterialEntryOperation", Component="RawMaterialEntryOperationView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=107 },
|
||||
// 原材料卡片
|
||||
new SysMenu{ Id=1300150010901, Pid=1300150000101, Title="原材料卡片", Path="/xslmes/mesXslRawMaterialCard", Name="mesXslRawMaterialCard", Component="RawMaterialCardListView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=108 },
|
||||
// 库区管理
|
||||
new SysMenu{ Id=1300150011001, Pid=1300150000101, Title="库区管理", Path="/xslmes/mesXslWarehouseArea", Name="mesXslWarehouseArea", Component="WarehouseAreaListView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=109 },
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ public class SysTenantMenuSeedData : ISqlSugarEntitySeedData<SysTenantMenu>
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010601},
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010701},
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010801},
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010901},
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150011001},
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200012101},
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200012111},
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200012121},
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
using System.Globalization;
|
||||
using YY.Admin.Core.Entity;
|
||||
|
||||
namespace YY.Admin.Core.Util;
|
||||
|
||||
/// <summary>
|
||||
/// 「已入场重量」桌面端本地累计计算器。
|
||||
/// 与后端 IMesXslRawMaterialEntryService.sumEnteredWeightByBillNos 保持同一口径:
|
||||
/// ① 把 totalPortions / portionWeight 按 "x/y/z/" 拆分
|
||||
/// ② 逐位 (份数 × 每份重量) 累加,得到该 entry 的小计
|
||||
/// ③ 同一榜单(BillNo)下所有 entry 的小计再次累加
|
||||
/// 任一位置解析失败或缺失:静默跳过,保证 UI 降级可用。
|
||||
/// </summary>
|
||||
public static class EnteredWeightCalculator
|
||||
{
|
||||
/// <summary>累计一条入场记录的小计。</summary>
|
||||
public static double SumOneEntry(string? totalPortions, string? portionWeight)
|
||||
{
|
||||
var portions = SplitJoined(totalPortions);
|
||||
var weights = SplitJoined(portionWeight);
|
||||
if (portions.Length == 0 || weights.Length == 0)
|
||||
{
|
||||
return 0d;
|
||||
}
|
||||
var n = Math.Min(portions.Length, weights.Length);
|
||||
double sum = 0d;
|
||||
for (var i = 0; i < n; i++)
|
||||
{
|
||||
if (!TryParse(portions[i], out var p) || !TryParse(weights[i], out var w))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
sum += p * w;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
/// <summary>按 BillNo 分组累计「已入场重量」。返回 Dictionary<BillNo, 总和>。</summary>
|
||||
public static Dictionary<string, double> SumByBillNos(
|
||||
IEnumerable<MesXslRawMaterialEntry> entries,
|
||||
IEnumerable<string?> billNos)
|
||||
{
|
||||
var keys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var b in billNos)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(b)) keys.Add(b!);
|
||||
}
|
||||
var result = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
|
||||
if (keys.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (var e in entries)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(e.BillNo) || !keys.Contains(e.BillNo!))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var sub = SumOneEntry(e.TotalPortions, e.PortionWeight);
|
||||
if (sub == 0d)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (result.TryGetValue(e.BillNo!, out var acc))
|
||||
{
|
||||
result[e.BillNo!] = acc + sub;
|
||||
}
|
||||
else
|
||||
{
|
||||
result[e.BillNo!] = sub;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string[] SplitJoined(string? value)
|
||||
{
|
||||
// 兼容 "x/y/z" 与 "x/y/z/" 两种格式;过滤空段
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
return value.Split('/')
|
||||
.Select(s => s.Trim())
|
||||
.Where(s => s.Length > 0)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static bool TryParse(string s, out double v) =>
|
||||
double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out v);
|
||||
}
|
||||
@@ -256,6 +256,67 @@ public class RawMaterialCardService : IRawMaterialCardService, ISingletonDepende
|
||||
return allSuccess;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 「重新拆码」专用:按 splitDetailId IN 批量删除卡片。
|
||||
/// 走后端 /anon/deleteBySplitDetailIds,server 端用 LambdaUpdateWrapper.in().remove() 一次完成。
|
||||
/// dryRun=true 时仅查询匹配数量、不删除(用于桌面端弹窗确认前的预提示)。
|
||||
/// 仅在线时调用——离线场景不支持本操作(涉及跨记录批量删,难以走 PendingOps 同步还原)。
|
||||
/// </summary>
|
||||
public async Task<int> DeleteBySplitDetailIdsAsync(IEnumerable<string> splitDetailIds, bool dryRun = false, CancellationToken ct = default)
|
||||
{
|
||||
if (splitDetailIds == null) return 0;
|
||||
var distinctIds = splitDetailIds
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||||
.Select(s => s.Trim())
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
if (distinctIds.Count == 0) return 0;
|
||||
|
||||
if (!_networkMonitor.IsOnline)
|
||||
{
|
||||
_logger.Warning("[原材料卡片] 当前离线,无法按拆码明细 ID 批量删除卡片");
|
||||
return -1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var idsQs = string.Join(",", distinctIds.Select(Uri.EscapeDataString));
|
||||
var url = $"{BaseUrl}/xslmes/mesXslRawMaterialCard/anon/deleteBySplitDetailIds"
|
||||
+ $"?splitDetailIds={idsQs}&dryRun={(dryRun ? "true" : "false")}&tenantId={DefaultTenantId}";
|
||||
using var client = CreateClient();
|
||||
var resp = await client.PostAsync(url, new StringContent(string.Empty), ct).ConfigureAwait(false);
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.Warning($"[原材料卡片] 按拆码明细ID批删 HTTP {(int)resp.StatusCode}");
|
||||
return -1;
|
||||
}
|
||||
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var code = doc.RootElement.TryGetProperty("code", out var codeEl) ? codeEl.GetInt32() : -1;
|
||||
if (code != 200) return -1;
|
||||
var count = doc.RootElement.TryGetProperty("result", out var resultEl) && resultEl.ValueKind == JsonValueKind.Number
|
||||
? resultEl.GetInt32()
|
||||
: 0;
|
||||
|
||||
if (!dryRun && count > 0)
|
||||
{
|
||||
// 同步清理本地缓存:把 SplitDetailId 在 distinctIds 中的卡片移除
|
||||
var idSet = new HashSet<string>(distinctIds, StringComparer.OrdinalIgnoreCase);
|
||||
lock (_cacheLock)
|
||||
{
|
||||
_localCache.RemoveAll(c => !string.IsNullOrWhiteSpace(c.SplitDetailId) && idSet.Contains(c.SplitDetailId!));
|
||||
SaveCacheToDiskUnsafe();
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[原材料卡片] 按拆码明细ID批删异常: {ex.Message}");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> UpdatePriorityAsync(string id, string priorityPickup, CancellationToken ct = default)
|
||||
{
|
||||
if (_networkMonitor.IsOnline)
|
||||
@@ -662,6 +723,7 @@ public class RawMaterialCardService : IRawMaterialCardService, ISingletonDepende
|
||||
{
|
||||
Id = input.Id,
|
||||
Barcode = input.Barcode,
|
||||
SplitDetailId = input.SplitDetailId,
|
||||
BatchNo = input.BatchNo,
|
||||
EntryDate = input.EntryDate,
|
||||
MaterialId = input.MaterialId,
|
||||
|
||||
@@ -237,6 +237,15 @@ public class RawMaterialEntryService : IRawMaterialEntryService, ISingletonDepen
|
||||
return null;
|
||||
}
|
||||
|
||||
public IReadOnlyList<MesXslRawMaterialEntry> GetCachedSnapshot()
|
||||
{
|
||||
// 注意:不允许直接返回 _localCache 引用,避免外部修改污染缓存;用 Clone 做深拷贝。
|
||||
lock (_cacheLock)
|
||||
{
|
||||
return _localCache.Select(Clone).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Remote ────────────────────────────────────────────────────────────────
|
||||
|
||||
private async Task<List<MesXslRawMaterialEntry>> FetchRemoteListAsync(CancellationToken ct)
|
||||
@@ -570,6 +579,9 @@ public class RawMaterialEntryService : IRawMaterialEntryService, ISingletonDepen
|
||||
SupplierName = e.SupplierName, ManufacturerMaterialName = e.ManufacturerMaterialName,
|
||||
ShelfLife = e.ShelfLife, TotalWeight = e.TotalWeight, TotalPortions = e.TotalPortions,
|
||||
PortionWeight = e.PortionWeight, PortionPackages = e.PortionPackages,
|
||||
PortionWarehouseLocations = e.PortionWarehouseLocations,
|
||||
PortionDetailIds = e.PortionDetailIds,
|
||||
PortionCardFlags = e.PortionCardFlags,
|
||||
TestResult = e.TestResult, TestStatus = e.TestStatus, PrintFlag = e.PrintFlag,
|
||||
StockBalance = e.StockBalance, WarehouseLocation = e.WarehouseLocation,
|
||||
UnloadOperator = e.UnloadOperator, IsSpecialAdoption = e.IsSpecialAdoption,
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.Net.Http;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Services;
|
||||
|
||||
namespace YY.Admin.Services.Service.Warehouse;
|
||||
|
||||
/// <summary>
|
||||
/// 仓库数据只读服务:启动时从后端拉取全量列表并缓存到磁盘,断网时回退本地缓存。
|
||||
/// 仅供其他模块的下拉筛选使用,不提供 CRUD。
|
||||
/// </summary>
|
||||
public class WarehouseService : IWarehouseService, ISingletonDependency
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly INetworkMonitor _networkMonitor;
|
||||
private readonly ILoggerService _logger;
|
||||
private readonly object _cacheLock = new();
|
||||
private readonly string _cacheFilePath;
|
||||
private List<MesXslWarehouse> _localCache = new();
|
||||
|
||||
private static readonly JsonSerializerOptions _jsonOpts = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
};
|
||||
|
||||
public WarehouseService(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IConfiguration configuration,
|
||||
INetworkMonitor networkMonitor,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_configuration = configuration;
|
||||
_networkMonitor = networkMonitor;
|
||||
_logger = logger;
|
||||
|
||||
var appDataDir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"YY.Admin", "sync-cache");
|
||||
Directory.CreateDirectory(appDataDir);
|
||||
_cacheFilePath = Path.Combine(appDataDir, "warehouse-cache.json");
|
||||
|
||||
LoadCacheFromDisk();
|
||||
_logger.Information($"[仓库数据] 服务初始化,本地缓存={_localCache.Count} 条");
|
||||
|
||||
if (_networkMonitor.IsOnline)
|
||||
_ = Task.Run(() => RefreshFromRemoteAsync(CancellationToken.None));
|
||||
|
||||
_networkMonitor.StatusChanged += isOnline =>
|
||||
{
|
||||
if (isOnline)
|
||||
_ = Task.Run(() => RefreshFromRemoteAsync(CancellationToken.None));
|
||||
};
|
||||
}
|
||||
|
||||
private string BaseUrl => (_configuration.GetValue<string>("JeecgIntegration:BaseUrl") ?? "http://localhost:8080/jeecg-boot").TrimEnd('/');
|
||||
private int DefaultTenantId => (int?)_configuration.GetValue<long?>("JeecgIntegration:DefaultTenantId") ?? 1002;
|
||||
|
||||
public async Task<List<MesXslWarehouse>> GetAllAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (_networkMonitor.IsOnline)
|
||||
{
|
||||
try
|
||||
{
|
||||
await RefreshFromRemoteAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
lock (_cacheLock)
|
||||
{
|
||||
return _localCache.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshFromRemoteAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = new List<MesXslWarehouse>();
|
||||
int pageNo = 1;
|
||||
const int pageSize = 500;
|
||||
while (true)
|
||||
{
|
||||
var url = $"{BaseUrl}/xslmes/mesXslWarehouse/anon/list?pageNo={pageNo}&pageSize={pageSize}&tenantId={DefaultTenantId}";
|
||||
using var client = _httpClientFactory.CreateClient("JeecgApi");
|
||||
var resp = await client.GetAsync(url, ct).ConfigureAwait(false);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (!doc.RootElement.TryGetProperty("result", out var resultEl)) break;
|
||||
if (resultEl.TryGetProperty("records", out var recordsEl))
|
||||
{
|
||||
var page = recordsEl.Deserialize<List<MesXslWarehouse>>(_jsonOpts);
|
||||
if (page != null) result.AddRange(page);
|
||||
}
|
||||
long total = 0;
|
||||
if (resultEl.TryGetProperty("total", out var totalEl)) total = totalEl.GetInt64();
|
||||
if (result.Count >= total || (resultEl.TryGetProperty("records", out var r2) && r2.GetArrayLength() < pageSize)) break;
|
||||
pageNo++;
|
||||
}
|
||||
|
||||
lock (_cacheLock)
|
||||
{
|
||||
_localCache = result;
|
||||
SaveCacheToDiskUnsafe();
|
||||
}
|
||||
_logger.Information($"[仓库数据] 远端刷新成功,共 {result.Count} 条");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[仓库数据] 远端刷新失败,使用缓存: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadCacheFromDisk()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_cacheFilePath)) return;
|
||||
var json = File.ReadAllText(_cacheFilePath);
|
||||
_localCache = JsonSerializer.Deserialize<List<MesXslWarehouse>>(json, _jsonOpts) ?? new();
|
||||
}
|
||||
catch { _localCache = new(); }
|
||||
}
|
||||
|
||||
private void SaveCacheToDiskUnsafe()
|
||||
{
|
||||
try { File.WriteAllText(_cacheFilePath, JsonSerializer.Serialize(_localCache, _jsonOpts)); } catch { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,779 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.Net.Http;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Web;
|
||||
using Prism.Events;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Events;
|
||||
using YY.Admin.Core.Services;
|
||||
|
||||
namespace YY.Admin.Services.Service.WarehouseArea;
|
||||
|
||||
public class WarehouseAreaService : IWarehouseAreaService, ISingletonDependency
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly INetworkMonitor _networkMonitor;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ILoggerService _logger;
|
||||
private readonly SemaphoreSlim _syncLock = new(1, 1);
|
||||
private readonly object _cacheLock = new();
|
||||
private readonly string _pendingOpsFilePath;
|
||||
private readonly string _cacheFilePath;
|
||||
private List<WarehouseAreaPendingOperation> _pendingOps = new();
|
||||
private List<MesXslWarehouseArea> _localCache = new();
|
||||
|
||||
private static readonly JsonSerializerOptions _jsonOpts = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
Converters = { new NullableDateTimeJsonConverter() }
|
||||
};
|
||||
|
||||
public WarehouseAreaService(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IConfiguration configuration,
|
||||
INetworkMonitor networkMonitor,
|
||||
IEventAggregator eventAggregator,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_configuration = configuration;
|
||||
_networkMonitor = networkMonitor;
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
|
||||
var appDataDir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"YY.Admin", "sync-cache");
|
||||
Directory.CreateDirectory(appDataDir);
|
||||
_pendingOpsFilePath = Path.Combine(appDataDir, "warehouse-area-pending-ops.json");
|
||||
_cacheFilePath = Path.Combine(appDataDir, "warehouse-area-cache.json");
|
||||
|
||||
LoadPendingOpsFromDisk();
|
||||
LoadCacheFromDisk();
|
||||
_logger.Information($"[库区同步] 服务初始化完成,缓存={_localCache.Count},待上传={_pendingOps.Count},在线={_networkMonitor.IsOnline}");
|
||||
|
||||
_networkMonitor.StatusChanged += OnNetworkStatusChanged;
|
||||
if (_networkMonitor.IsOnline)
|
||||
_ = Task.Run(() => SyncAfterReconnectAsync(CancellationToken.None));
|
||||
}
|
||||
|
||||
private const int MaxPendingRetries = 5;
|
||||
private string BaseUrl => (_configuration.GetValue<string>("JeecgIntegration:BaseUrl") ?? "http://localhost:8080/jeecg-boot").TrimEnd('/');
|
||||
private int DefaultTenantId => (int?)_configuration.GetValue<long?>("JeecgIntegration:DefaultTenantId") ?? 1002;
|
||||
private HttpClient CreateClient() => _httpClientFactory.CreateClient("JeecgApi");
|
||||
|
||||
// ─────────────────── 分页 ───────────────────
|
||||
|
||||
public async Task<WarehouseAreaPageResult> PageAsync(
|
||||
int pageNo, int pageSize,
|
||||
string? areaCode = null,
|
||||
string? areaName = null,
|
||||
string? warehouseId = null,
|
||||
string? status = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
List<MesXslWarehouseArea>? source = null;
|
||||
if (_networkMonitor.IsOnline)
|
||||
{
|
||||
try
|
||||
{
|
||||
source = await FetchRemoteListAsync(ct).ConfigureAwait(false);
|
||||
lock (_cacheLock)
|
||||
{
|
||||
_localCache = source.Select(Clone).ToList();
|
||||
SaveCacheToDiskUnsafe();
|
||||
}
|
||||
_logger.Information($"[库区列表] 远端拉取成功 count={source.Count}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
source = null;
|
||||
_logger.Warning($"[库区列表] 远端拉取失败,回退本地缓存:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
lock (_cacheLock)
|
||||
{
|
||||
source ??= _localCache.Select(Clone).ToList();
|
||||
source = ApplyPendingOpsSnapshotUnsafe(source);
|
||||
}
|
||||
|
||||
var filtered = ApplyFilters(source, areaCode, areaName, warehouseId, status);
|
||||
var total = filtered.Count;
|
||||
var records = filtered.Skip(Math.Max(0, (pageNo - 1) * pageSize)).Take(pageSize).ToList();
|
||||
return new WarehouseAreaPageResult(records, total, pageNo, pageSize);
|
||||
}
|
||||
|
||||
// ─────────────────── 单查 ───────────────────
|
||||
|
||||
public async Task<MesXslWarehouseArea?> GetByIdAsync(string id, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id)) return null;
|
||||
|
||||
if (_networkMonitor.IsOnline)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}/xslmes/mesXslWarehouseArea/anon/queryById?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}";
|
||||
using var client = CreateClient();
|
||||
var resp = await client.GetAsync(url, ct).ConfigureAwait(false);
|
||||
if (resp.IsSuccessStatusCode)
|
||||
{
|
||||
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (doc.RootElement.TryGetProperty("result", out var resultEl) && resultEl.ValueKind != JsonValueKind.Null)
|
||||
return resultEl.Deserialize<MesXslWarehouseArea>(_jsonOpts);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[库区详情] 远端查询异常 id={id}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
lock (_cacheLock)
|
||||
return _localCache.FirstOrDefault(v => string.Equals(v.Id, id, StringComparison.OrdinalIgnoreCase)) is { } found
|
||||
? Clone(found) : null;
|
||||
}
|
||||
|
||||
// ─────────────────── 新增 ───────────────────
|
||||
|
||||
public async Task<bool> AddAsync(MesXslWarehouseArea area, CancellationToken ct = default)
|
||||
{
|
||||
if (area == null) return false;
|
||||
if (!area.TenantId.HasValue || area.TenantId.Value <= 0)
|
||||
area.TenantId = DefaultTenantId;
|
||||
|
||||
var local = Clone(area);
|
||||
if (string.IsNullOrWhiteSpace(local.Id))
|
||||
local.Id = $"local-{Guid.NewGuid():N}";
|
||||
|
||||
if (_networkMonitor.IsOnline)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ok = await RemoteAddAsync(local, ct).ConfigureAwait(false);
|
||||
if (ok) { UpsertLocalCache(local); return true; }
|
||||
_logger.Warning($"[库区新增] 远端返回失败 id={local.Id}");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[库区新增] 远端异常,转离线入队:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
EnqueuePendingOperation(new WarehouseAreaPendingOperation
|
||||
{
|
||||
OpType = WarehouseAreaOperationType.Add,
|
||||
AreaId = local.Id,
|
||||
Entity = local
|
||||
});
|
||||
UpsertLocalCache(local);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─────────────────── 编辑 ───────────────────
|
||||
|
||||
public async Task<bool> EditAsync(MesXslWarehouseArea area, CancellationToken ct = default)
|
||||
{
|
||||
if (area == null || string.IsNullOrWhiteSpace(area.Id)) return false;
|
||||
if (!area.TenantId.HasValue || area.TenantId.Value <= 0)
|
||||
area.TenantId = DefaultTenantId;
|
||||
|
||||
var local = Clone(area);
|
||||
if (IsLocalTempId(local.Id))
|
||||
{
|
||||
if (TryMergeIntoPendingAdd(local)) { UpsertLocalCache(local); return true; }
|
||||
EnqueuePendingOperation(new WarehouseAreaPendingOperation
|
||||
{
|
||||
OpType = WarehouseAreaOperationType.Add,
|
||||
AreaId = local.Id,
|
||||
Entity = local
|
||||
});
|
||||
UpsertLocalCache(local);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_networkMonitor.IsOnline)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (ok, _) = await RemoteEditAsync(local, ct).ConfigureAwait(false);
|
||||
if (ok) { UpsertLocalCache(local); return true; }
|
||||
_logger.Warning($"[库区修改] 远端返回失败 id={local.Id}");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[库区修改] 远端异常,转离线入队:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
EnqueuePendingOperation(new WarehouseAreaPendingOperation
|
||||
{
|
||||
OpType = WarehouseAreaOperationType.Edit,
|
||||
AreaId = local.Id,
|
||||
Entity = local,
|
||||
AnchorUpdateTime = local.UpdateTime
|
||||
});
|
||||
UpsertLocalCache(local);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─────────────────── 删除 ───────────────────
|
||||
|
||||
public async Task<bool> DeleteAsync(string id, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id)) return false;
|
||||
|
||||
if (_networkMonitor.IsOnline)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ok = await RemoteDeleteAsync(id, ct).ConfigureAwait(false);
|
||||
if (ok) { RemoveFromLocalCache(id); return true; }
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[库区删除] 远端异常,转离线入队:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
DateTime? anchor;
|
||||
lock (_cacheLock)
|
||||
anchor = _localCache.FirstOrDefault(v => string.Equals(v.Id, id, StringComparison.OrdinalIgnoreCase))?.UpdateTime;
|
||||
|
||||
EnqueuePendingOperation(new WarehouseAreaPendingOperation
|
||||
{
|
||||
OpType = WarehouseAreaOperationType.Delete,
|
||||
AreaId = id,
|
||||
AnchorUpdateTime = anchor
|
||||
});
|
||||
RemoveFromLocalCache(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─────────────────── 启停 ───────────────────
|
||||
|
||||
public async Task<bool> UpdateStatusAsync(string id, string newStatus, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id)) return false;
|
||||
if (newStatus != "0" && newStatus != "1") return false;
|
||||
|
||||
if (_networkMonitor.IsOnline)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}/xslmes/mesXslWarehouseArea/anon/updateStatus?id={Uri.EscapeDataString(id)}&status={newStatus}&tenantId={DefaultTenantId}";
|
||||
using var client = CreateClient();
|
||||
var resp = await client.PostAsync(url, new StringContent(string.Empty), ct).ConfigureAwait(false);
|
||||
if (resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false))
|
||||
{
|
||||
lock (_cacheLock)
|
||||
{
|
||||
var cached = _localCache.FirstOrDefault(v => string.Equals(v.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||
if (cached != null) cached.Status = newStatus;
|
||||
SaveCacheToDiskUnsafe();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[库区状态] 远端异常,转离线入队:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
EnqueuePendingOperation(new WarehouseAreaPendingOperation
|
||||
{
|
||||
OpType = WarehouseAreaOperationType.UpdateStatus,
|
||||
AreaId = id,
|
||||
NewStatus = newStatus
|
||||
});
|
||||
lock (_cacheLock)
|
||||
{
|
||||
var cached = _localCache.FirstOrDefault(v => string.Equals(v.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||
if (cached != null) cached.Status = newStatus;
|
||||
SaveCacheToDiskUnsafe();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─────────────────── 编码校验 ───────────────────
|
||||
|
||||
public async Task<bool> CheckAreaCodeAsync(string areaCode, string? excludeId, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(areaCode)) return true;
|
||||
|
||||
if (!_networkMonitor.IsOnline)
|
||||
{
|
||||
lock (_cacheLock)
|
||||
return !_localCache.Any(v =>
|
||||
string.Equals(v.AreaCode, areaCode.Trim(), StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.Equals(v.Id, excludeId, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var query = HttpUtility.ParseQueryString(string.Empty);
|
||||
query["areaCode"] = areaCode.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(excludeId)) query["dataId"] = excludeId;
|
||||
query["tenantId"] = DefaultTenantId.ToString();
|
||||
var url = $"{BaseUrl}/xslmes/mesXslWarehouseArea/anon/checkAreaCode?{query}";
|
||||
using var client = CreateClient();
|
||||
var resp = await client.GetAsync(url, ct).ConfigureAwait(false);
|
||||
if (!resp.IsSuccessStatusCode) return false;
|
||||
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (doc.RootElement.TryGetProperty("code", out var codeEl)) return codeEl.GetInt32() == 200;
|
||||
if (doc.RootElement.TryGetProperty("success", out var successEl)) return successEl.GetBoolean();
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[库区编码校验] 远端异常,回退本地校验:{ex.Message}");
|
||||
lock (_cacheLock)
|
||||
return !_localCache.Any(v =>
|
||||
string.Equals(v.AreaCode, areaCode.Trim(), StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.Equals(v.Id, excludeId, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────── 远端 HTTP ───────────────────
|
||||
|
||||
private async Task<List<MesXslWarehouseArea>> FetchRemoteListAsync(CancellationToken ct)
|
||||
{
|
||||
var query = HttpUtility.ParseQueryString(string.Empty);
|
||||
query["pageNo"] = "1";
|
||||
query["pageSize"] = "10000";
|
||||
query["tenantId"] = DefaultTenantId.ToString();
|
||||
var url = $"{BaseUrl}/xslmes/mesXslWarehouseArea/anon/list?{query}";
|
||||
using var client = CreateClient();
|
||||
_logger.Information($"[库区远端] GET {url}");
|
||||
var resp = await client.GetAsync(url, ct).ConfigureAwait(false);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var result = doc.RootElement.GetProperty("result");
|
||||
return result.GetProperty("records").Deserialize<List<MesXslWarehouseArea>>(_jsonOpts) ?? new();
|
||||
}
|
||||
|
||||
private async Task<bool> RemoteAddAsync(MesXslWarehouseArea entity, CancellationToken ct)
|
||||
{
|
||||
var url = $"{BaseUrl}/xslmes/mesXslWarehouseArea/anon/add?tenantId={DefaultTenantId}";
|
||||
var payload = Clone(entity);
|
||||
if (IsLocalTempId(payload.Id)) payload.Id = null;
|
||||
return await PostJsonAsync(url, payload, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<(bool Ok, bool IsVersionConflict)> RemoteEditAsync(MesXslWarehouseArea entity, CancellationToken ct)
|
||||
{
|
||||
var url = $"{BaseUrl}/xslmes/mesXslWarehouseArea/anon/edit?tenantId={DefaultTenantId}";
|
||||
return await PostJsonCheckVersionAsync(url, entity, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<bool> RemoteDeleteAsync(string id, CancellationToken ct)
|
||||
{
|
||||
var url = $"{BaseUrl}/xslmes/mesXslWarehouseArea/anon/delete?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}";
|
||||
using var client = CreateClient();
|
||||
var resp = await client.DeleteAsync(url, ct).ConfigureAwait(false);
|
||||
return resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<bool> PostJsonAsync(string url, object body, CancellationToken ct)
|
||||
{
|
||||
var content = new StringContent(JsonSerializer.Serialize(body, _jsonOpts), Encoding.UTF8, "application/json");
|
||||
using var client = CreateClient();
|
||||
var resp = await client.PostAsync(url, content, ct).ConfigureAwait(false);
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.Warning($"[库区] POST {url} HTTP {(int)resp.StatusCode}");
|
||||
return false;
|
||||
}
|
||||
return await IsSuccessResultAsync(resp, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<(bool Ok, bool IsVersionConflict)> PostJsonCheckVersionAsync(string url, object body, CancellationToken ct)
|
||||
{
|
||||
var content = new StringContent(JsonSerializer.Serialize(body, _jsonOpts), Encoding.UTF8, "application/json");
|
||||
using var client = CreateClient();
|
||||
var resp = await client.PostAsync(url, content, ct).ConfigureAwait(false);
|
||||
if (!resp.IsSuccessStatusCode) return (false, false);
|
||||
try
|
||||
{
|
||||
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
int code = 200;
|
||||
if (doc.RootElement.TryGetProperty("code", out var codeEl)) code = codeEl.GetInt32();
|
||||
if (code == 200) return (true, false);
|
||||
if (doc.RootElement.TryGetProperty("message", out var msgEl) && (msgEl.GetString() ?? "").Contains("已被他人修改"))
|
||||
return (false, true);
|
||||
return (false, false);
|
||||
}
|
||||
catch { return (true, false); }
|
||||
}
|
||||
|
||||
private static async Task<bool> IsSuccessResultAsync(HttpResponseMessage resp, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (doc.RootElement.TryGetProperty("code", out var code)) return code.GetInt32() == 200;
|
||||
if (doc.RootElement.TryGetProperty("success", out var success)) return success.GetBoolean();
|
||||
return true;
|
||||
}
|
||||
catch { return true; }
|
||||
}
|
||||
|
||||
// ─────────────────── 重连同步 ───────────────────
|
||||
|
||||
private void OnNetworkStatusChanged(bool isOnline)
|
||||
{
|
||||
if (!isOnline) return;
|
||||
_ = Task.Run(() => SyncAfterReconnectAsync(CancellationToken.None));
|
||||
}
|
||||
|
||||
private async Task SyncAfterReconnectAsync(CancellationToken ct)
|
||||
{
|
||||
_logger.Information("[库区重连] 开始重连同步");
|
||||
var pushResult = await PushPendingOnReconnectAsync(ct).ConfigureAwait(false);
|
||||
if (!_networkMonitor.IsOnline) return;
|
||||
try
|
||||
{
|
||||
var remote = await FetchRemoteListAsync(ct).ConfigureAwait(false);
|
||||
lock (_cacheLock) { _localCache = remote.Select(Clone).ToList(); SaveCacheToDiskUnsafe(); }
|
||||
_eventAggregator.GetEvent<WarehouseAreaChangedEvent>().Publish(new WarehouseAreaChangedPayload { Action = "pull" });
|
||||
_logger.Information($"[库区重连] 全量回拉成功 count={remote.Count}");
|
||||
}
|
||||
catch (Exception ex) { _logger.Warning($"[库区重连] 全量回拉失败:{ex.Message}"); }
|
||||
|
||||
var hasActivity = pushResult.PushedCount > 0 || pushResult.ConflictCount > 0 || pushResult.NewRecordsPushed > 0;
|
||||
if (hasActivity)
|
||||
_eventAggregator.GetEvent<SyncConflictEvent>().Publish(new SyncConflictPayload
|
||||
{
|
||||
EntityName = "库区",
|
||||
PushedCount = pushResult.PushedCount,
|
||||
ConflictCount = pushResult.ConflictCount,
|
||||
NewRecordsPushed = pushResult.NewRecordsPushed
|
||||
});
|
||||
}
|
||||
|
||||
private sealed record PendingReplayResult(bool Ok, bool IsConflict, string? EntityId);
|
||||
|
||||
private async Task<PushPendingResult> PushPendingOnReconnectAsync(CancellationToken ct)
|
||||
{
|
||||
if (!await _syncLock.WaitAsync(0, ct).ConfigureAwait(false)) return new PushPendingResult(0, 0, 0);
|
||||
try
|
||||
{
|
||||
List<WarehouseAreaPendingOperation> snapshot;
|
||||
lock (_cacheLock) { snapshot = _pendingOps.OrderBy(x => x.CreatedAt).ToList(); }
|
||||
_logger.Information($"[库区推送] 开始推送 pending={snapshot.Count}");
|
||||
|
||||
int pushed = 0, conflicts = 0, newPushed = 0;
|
||||
foreach (var op in snapshot)
|
||||
{
|
||||
if (!_networkMonitor.IsOnline) break;
|
||||
lock (_cacheLock) { if (!_pendingOps.Any(x => x.Id == op.Id)) continue; }
|
||||
|
||||
var result = await ExecutePendingOperationAsync(op, ct).ConfigureAwait(false);
|
||||
if (!result.Ok)
|
||||
{
|
||||
lock (_cacheLock)
|
||||
{
|
||||
op.RetryCount++;
|
||||
if (op.RetryCount >= MaxPendingRetries)
|
||||
{
|
||||
_pendingOps.RemoveAll(x => x.Id == op.Id);
|
||||
SavePendingOpsToDiskUnsafe();
|
||||
continue;
|
||||
}
|
||||
SavePendingOpsToDiskUnsafe();
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (result.IsConflict)
|
||||
{
|
||||
conflicts++;
|
||||
if (!string.IsNullOrWhiteSpace(result.EntityId)) RemovePendingOpsByEntityId(result.EntityId!);
|
||||
continue;
|
||||
}
|
||||
lock (_cacheLock)
|
||||
{
|
||||
if (op.OpType == WarehouseAreaOperationType.Add) newPushed++; else pushed++;
|
||||
_pendingOps.RemoveAll(x => x.Id == op.Id);
|
||||
SavePendingOpsToDiskUnsafe();
|
||||
}
|
||||
}
|
||||
return new PushPendingResult(pushed, conflicts, newPushed);
|
||||
}
|
||||
finally { _syncLock.Release(); }
|
||||
}
|
||||
|
||||
private async Task<PendingReplayResult> ExecutePendingOperationAsync(WarehouseAreaPendingOperation op, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (op.OpType)
|
||||
{
|
||||
case WarehouseAreaOperationType.Add:
|
||||
{
|
||||
var ok = op.Entity != null && await RemoteAddAsync(op.Entity, ct).ConfigureAwait(false);
|
||||
return ok ? new PendingReplayResult(true, false, op.AreaId) : new PendingReplayResult(false, false, null);
|
||||
}
|
||||
case WarehouseAreaOperationType.Edit:
|
||||
{
|
||||
if (op.Entity?.Id == null) return new PendingReplayResult(false, false, null);
|
||||
var remote = await GetByIdAsync(op.Entity.Id, ct).ConfigureAwait(false);
|
||||
if (remote != null && op.AnchorUpdateTime != null && remote.UpdateTime != op.AnchorUpdateTime)
|
||||
{
|
||||
UpsertLocalCache(remote);
|
||||
return new PendingReplayResult(true, true, op.Entity.Id);
|
||||
}
|
||||
var (ok, isConflict) = await RemoteEditAsync(op.Entity, ct).ConfigureAwait(false);
|
||||
if (isConflict)
|
||||
{
|
||||
var fresh = await GetByIdAsync(op.Entity.Id, ct).ConfigureAwait(false);
|
||||
if (fresh != null) UpsertLocalCache(fresh);
|
||||
return new PendingReplayResult(true, true, op.Entity.Id);
|
||||
}
|
||||
return ok ? new PendingReplayResult(true, false, op.Entity.Id) : new PendingReplayResult(false, false, null);
|
||||
}
|
||||
case WarehouseAreaOperationType.Delete:
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(op.AreaId)) return new PendingReplayResult(false, false, null);
|
||||
var id = op.AreaId!;
|
||||
var remote = await GetByIdAsync(id, ct).ConfigureAwait(false);
|
||||
if (remote == null) return new PendingReplayResult(true, false, id);
|
||||
if (op.AnchorUpdateTime != null && remote.UpdateTime != op.AnchorUpdateTime)
|
||||
{
|
||||
UpsertLocalCache(remote);
|
||||
return new PendingReplayResult(true, true, id);
|
||||
}
|
||||
var ok = await RemoteDeleteAsync(id, ct).ConfigureAwait(false);
|
||||
return ok ? new PendingReplayResult(true, false, id) : new PendingReplayResult(false, false, null);
|
||||
}
|
||||
case WarehouseAreaOperationType.UpdateStatus:
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(op.AreaId) || string.IsNullOrWhiteSpace(op.NewStatus))
|
||||
return new PendingReplayResult(false, false, null);
|
||||
var url = $"{BaseUrl}/xslmes/mesXslWarehouseArea/anon/updateStatus?id={Uri.EscapeDataString(op.AreaId!)}&status={op.NewStatus}&tenantId={DefaultTenantId}";
|
||||
using var client = CreateClient();
|
||||
var resp = await client.PostAsync(url, new StringContent(string.Empty), ct).ConfigureAwait(false);
|
||||
var ok = resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false);
|
||||
return ok ? new PendingReplayResult(true, false, op.AreaId) : new PendingReplayResult(false, false, null);
|
||||
}
|
||||
default: return new PendingReplayResult(true, false, null);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[库区推送] 执行异常 op={op.OpType}: {ex.Message}");
|
||||
return new PendingReplayResult(false, false, null);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────── 过滤 / 缓存辅助 ───────────────────
|
||||
|
||||
private static List<MesXslWarehouseArea> ApplyFilters(
|
||||
List<MesXslWarehouseArea> source,
|
||||
string? areaCode, string? areaName, string? warehouseId, string? status)
|
||||
{
|
||||
IEnumerable<MesXslWarehouseArea> q = source;
|
||||
if (!string.IsNullOrWhiteSpace(areaCode))
|
||||
q = q.Where(v => (v.AreaCode ?? "").Contains(areaCode, StringComparison.OrdinalIgnoreCase));
|
||||
if (!string.IsNullOrWhiteSpace(areaName))
|
||||
q = q.Where(v => (v.AreaName ?? "").Contains(areaName, StringComparison.OrdinalIgnoreCase));
|
||||
if (!string.IsNullOrWhiteSpace(warehouseId))
|
||||
q = q.Where(v => string.Equals(v.WarehouseId, warehouseId, StringComparison.OrdinalIgnoreCase));
|
||||
if (!string.IsNullOrWhiteSpace(status))
|
||||
q = q.Where(v => string.Equals(v.Status, status, StringComparison.OrdinalIgnoreCase));
|
||||
return q.OrderByDescending(v => v.CreateTime ?? DateTime.MinValue).ToList();
|
||||
}
|
||||
|
||||
private List<MesXslWarehouseArea> ApplyPendingOpsSnapshotUnsafe(List<MesXslWarehouseArea> source)
|
||||
{
|
||||
var map = source.Where(v => !string.IsNullOrWhiteSpace(v.Id))
|
||||
.ToDictionary(v => v.Id!, Clone, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var op in _pendingOps.OrderBy(x => x.CreatedAt))
|
||||
{
|
||||
switch (op.OpType)
|
||||
{
|
||||
case WarehouseAreaOperationType.Add:
|
||||
case WarehouseAreaOperationType.Edit:
|
||||
if (op.Entity?.Id != null) map[op.Entity.Id] = Clone(op.Entity);
|
||||
break;
|
||||
case WarehouseAreaOperationType.Delete:
|
||||
if (!string.IsNullOrWhiteSpace(op.AreaId)) map.Remove(op.AreaId);
|
||||
break;
|
||||
case WarehouseAreaOperationType.UpdateStatus:
|
||||
if (!string.IsNullOrWhiteSpace(op.AreaId) && map.TryGetValue(op.AreaId, out var entry))
|
||||
entry.Status = op.NewStatus;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return map.Values.ToList();
|
||||
}
|
||||
|
||||
private void EnqueuePendingOperation(WarehouseAreaPendingOperation op)
|
||||
{
|
||||
lock (_cacheLock) { _pendingOps.Add(op); SavePendingOpsToDiskUnsafe(); }
|
||||
}
|
||||
|
||||
private bool TryMergeIntoPendingAdd(MesXslWarehouseArea local)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(local.Id)) return false;
|
||||
lock (_cacheLock)
|
||||
{
|
||||
var pendingAdd = _pendingOps
|
||||
.Where(x => x.OpType == WarehouseAreaOperationType.Add)
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.FirstOrDefault(x =>
|
||||
string.Equals(x.AreaId, local.Id, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(x.Entity?.Id, local.Id, StringComparison.OrdinalIgnoreCase));
|
||||
if (pendingAdd == null) return false;
|
||||
pendingAdd.Entity = Clone(local);
|
||||
pendingAdd.AreaId = local.Id;
|
||||
SavePendingOpsToDiskUnsafe();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpsertLocalCache(MesXslWarehouseArea entity)
|
||||
{
|
||||
lock (_cacheLock)
|
||||
{
|
||||
var idx = _localCache.FindIndex(v => string.Equals(v.Id, entity.Id, StringComparison.OrdinalIgnoreCase));
|
||||
if (idx >= 0) _localCache[idx] = Clone(entity); else _localCache.Insert(0, Clone(entity));
|
||||
SaveCacheToDiskUnsafe();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveFromLocalCache(string id)
|
||||
{
|
||||
lock (_cacheLock) { _localCache.RemoveAll(v => string.Equals(v.Id, id, StringComparison.OrdinalIgnoreCase)); SaveCacheToDiskUnsafe(); }
|
||||
}
|
||||
|
||||
private void RemovePendingOpsByEntityId(string id)
|
||||
{
|
||||
lock (_cacheLock)
|
||||
{
|
||||
_pendingOps.RemoveAll(x =>
|
||||
(!string.IsNullOrWhiteSpace(x.AreaId) && string.Equals(x.AreaId, id, StringComparison.OrdinalIgnoreCase)) ||
|
||||
(x.Entity?.Id != null && string.Equals(x.Entity.Id, id, StringComparison.OrdinalIgnoreCase)));
|
||||
SavePendingOpsToDiskUnsafe();
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadPendingOpsFromDisk()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_pendingOpsFilePath)) return;
|
||||
var data = JsonSerializer.Deserialize<List<WarehouseAreaPendingOperation>>(File.ReadAllText(_pendingOpsFilePath), _jsonOpts);
|
||||
_pendingOps = data ?? new();
|
||||
}
|
||||
catch { _pendingOps = new(); }
|
||||
}
|
||||
|
||||
private void LoadCacheFromDisk()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_cacheFilePath)) return;
|
||||
var data = JsonSerializer.Deserialize<List<MesXslWarehouseArea>>(File.ReadAllText(_cacheFilePath), _jsonOpts);
|
||||
_localCache = data ?? new();
|
||||
}
|
||||
catch { _localCache = new(); }
|
||||
}
|
||||
|
||||
private void SavePendingOpsToDiskUnsafe() =>
|
||||
File.WriteAllText(_pendingOpsFilePath, JsonSerializer.Serialize(_pendingOps, _jsonOpts));
|
||||
|
||||
private void SaveCacheToDiskUnsafe() =>
|
||||
File.WriteAllText(_cacheFilePath, JsonSerializer.Serialize(_localCache, _jsonOpts));
|
||||
|
||||
private static MesXslWarehouseArea Clone(MesXslWarehouseArea input) => new()
|
||||
{
|
||||
Id = input.Id,
|
||||
AreaCode = input.AreaCode,
|
||||
AreaName = input.AreaName,
|
||||
WarehouseId = input.WarehouseId,
|
||||
WarehouseName = input.WarehouseName,
|
||||
WarehouseCategory = input.WarehouseCategory,
|
||||
WarehouseCategoryName = input.WarehouseCategoryName,
|
||||
MaxCapacity = input.MaxCapacity,
|
||||
ActualCapacity = input.ActualCapacity,
|
||||
Remark = input.Remark,
|
||||
Status = input.Status,
|
||||
TenantId = input.TenantId,
|
||||
CreateBy = input.CreateBy,
|
||||
CreateTime = input.CreateTime,
|
||||
UpdateBy = input.UpdateBy,
|
||||
UpdateTime = input.UpdateTime
|
||||
};
|
||||
|
||||
private static bool IsLocalTempId(string? id) =>
|
||||
!string.IsNullOrWhiteSpace(id) && id.StartsWith("local-", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// ─────────────────── 内部数据结构 ───────────────────
|
||||
|
||||
private sealed class WarehouseAreaPendingOperation
|
||||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||
public WarehouseAreaOperationType OpType { get; set; }
|
||||
public string? AreaId { get; set; }
|
||||
public MesXslWarehouseArea? Entity { get; set; }
|
||||
public DateTime? AnchorUpdateTime { get; set; }
|
||||
public string? NewStatus { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public int RetryCount { get; set; } = 0;
|
||||
}
|
||||
|
||||
private enum WarehouseAreaOperationType { Add = 1, Edit = 2, Delete = 3, UpdateStatus = 4 }
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
// 兼容 Jeecg 常见时间字符串格式(yyyy-MM-dd HH:mm:ss 等)。
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
private sealed class NullableDateTimeJsonConverter : JsonConverter<DateTime?>
|
||||
{
|
||||
private static readonly string[] SupportedFormats =
|
||||
[
|
||||
"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm:ss.fff",
|
||||
"yyyy-MM-ddTHH:mm:ss", "yyyy-MM-ddTHH:mm:ss.fff",
|
||||
"yyyy-MM-ddTHH:mm:ssZ", "yyyy-MM-ddTHH:mm:ss.fffZ",
|
||||
"yyyy-MM-dd"
|
||||
];
|
||||
|
||||
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null) return null;
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
var raw = reader.GetString();
|
||||
if (string.IsNullOrWhiteSpace(raw)) return null;
|
||||
if (DateTime.TryParseExact(raw, SupportedFormats,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
System.Globalization.DateTimeStyles.AssumeLocal, out var exact)) return exact;
|
||||
if (DateTime.TryParse(raw, out var fallback)) return fallback;
|
||||
}
|
||||
throw new JsonException($"无法转换为 DateTime?,token={reader.TokenType}");
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value.HasValue) writer.WriteStringValue(value.Value.ToString("yyyy-MM-dd"));
|
||||
else writer.WriteNullValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using Prism.Events;
|
||||
using System.Text.Json;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Events;
|
||||
using YY.Admin.Core.Services;
|
||||
|
||||
namespace YY.Admin.Services.Service.WarehouseArea;
|
||||
|
||||
/// <summary>
|
||||
/// 监听 STOMP 收到的库区变更信号,转发为桌面端 Prism 事件,触发列表刷新。
|
||||
/// </summary>
|
||||
public class WarehouseAreaSyncCoordinator : ISingletonDependency
|
||||
{
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ILoggerService _logger;
|
||||
private SubscriptionToken? _remoteCommandToken;
|
||||
private SubscriptionToken? _networkStatusToken;
|
||||
|
||||
public WarehouseAreaSyncCoordinator(
|
||||
IEventAggregator eventAggregator,
|
||||
SyncPollManager pollManager,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
|
||||
_remoteCommandToken = _eventAggregator
|
||||
.GetEvent<RemoteCommandReceivedEvent>()
|
||||
.Subscribe(OnRemoteCommand, ThreadOption.BackgroundThread);
|
||||
|
||||
_networkStatusToken = _eventAggregator
|
||||
.GetEvent<NetworkStatusChangedEvent>()
|
||||
.Subscribe(OnNetworkStatusChanged, ThreadOption.BackgroundThread);
|
||||
|
||||
pollManager.Register("库区", () =>
|
||||
{
|
||||
_eventAggregator.GetEvent<WarehouseAreaChangedEvent>()
|
||||
.Publish(new WarehouseAreaChangedPayload { Action = "poll" });
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
_logger.Information("[库区推送] WarehouseAreaSyncCoordinator 已启动");
|
||||
}
|
||||
|
||||
private void OnNetworkStatusChanged(NetworkStatusChangedPayload payload)
|
||||
{
|
||||
if (!payload.IsOnline) return;
|
||||
_logger.Information("[库区推送] 网络恢复,触发补偿刷新");
|
||||
_eventAggregator.GetEvent<WarehouseAreaChangedEvent>().Publish(new WarehouseAreaChangedPayload { Action = "reconnect" });
|
||||
}
|
||||
|
||||
private void OnRemoteCommand(RemoteCommandPayload payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = payload.CommandJson ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(json)) return;
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (!doc.RootElement.TryGetProperty("cmd", out var cmdEl)) return;
|
||||
|
||||
var cmd = cmdEl.GetString() ?? string.Empty;
|
||||
if (!cmd.Equals("MES_WAREHOUSE_AREA_CHANGED", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.Information($"[库区推送] 非库区命令 cmd={cmd},忽略");
|
||||
return;
|
||||
}
|
||||
|
||||
doc.RootElement.TryGetProperty("action", out var actionEl);
|
||||
doc.RootElement.TryGetProperty("warehouseAreaId", out var idEl);
|
||||
|
||||
var changedPayload = new WarehouseAreaChangedPayload
|
||||
{
|
||||
Action = actionEl.GetString() ?? string.Empty,
|
||||
WarehouseAreaId = idEl.ValueKind == JsonValueKind.String ? idEl.GetString() : null,
|
||||
};
|
||||
|
||||
_logger.Information($"收到库区变更信号: action={changedPayload.Action}, warehouseAreaId={changedPayload.WarehouseAreaId}");
|
||||
_eventAggregator.GetEvent<WarehouseAreaChangedEvent>().Publish(changedPayload);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"处理 STOMP 库区变更信号失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using YY.Admin.Core;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Events;
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.Core.Util;
|
||||
|
||||
namespace YY.Admin.Services.Service.WeightRecord;
|
||||
|
||||
@@ -20,6 +21,9 @@ public class WeightRecordService : IWeightRecordService, ISingletonDependency
|
||||
private readonly INetworkMonitor _networkMonitor;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ILoggerService _logger;
|
||||
// 用于按 BillNo 实时累计「已入场重量」,从本地入场记录缓存推导,
|
||||
// 与后端 IMesXslRawMaterialEntryService.sumEnteredWeightByBillNos 同一口径。
|
||||
private readonly IRawMaterialEntryService _rawMaterialEntryService;
|
||||
private readonly SemaphoreSlim _syncLock = new(1, 1);
|
||||
private readonly object _cacheLock = new();
|
||||
private readonly string _pendingOpsFilePath;
|
||||
@@ -39,13 +43,15 @@ public class WeightRecordService : IWeightRecordService, ISingletonDependency
|
||||
IConfiguration configuration,
|
||||
INetworkMonitor networkMonitor,
|
||||
IEventAggregator eventAggregator,
|
||||
ILoggerService logger)
|
||||
ILoggerService logger,
|
||||
IRawMaterialEntryService rawMaterialEntryService)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_configuration = configuration;
|
||||
_networkMonitor = networkMonitor;
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
_rawMaterialEntryService = rawMaterialEntryService;
|
||||
|
||||
var appDataDir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
@@ -107,11 +113,49 @@ public class WeightRecordService : IWeightRecordService, ISingletonDependency
|
||||
var filtered = ApplyFilters(source, filterBillNo, filterPlateNumber, filterInoutDirection, filterDriverName, filterMixerMaterialName);
|
||||
var total = filtered.Count;
|
||||
var records = filtered.Skip(Math.Max(0, (pageNo - 1) * pageSize)).Take(pageSize).ToList();
|
||||
// 当前页结果按本地入场记录缓存,按 BillNo 实时累计「已入场重量」(与后端口径一致)。
|
||||
// 放在分页之后做:避免对全量 source 做不必要的计算。
|
||||
FillEnteredWeightFromLocalEntries(records);
|
||||
return new WeightRecordPageResult(records, total, pageNo, pageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 给一批磅单记录批量填充「已入场重量」。
|
||||
/// 数据来源:本地 RawMaterialEntry 缓存的拆码明细字段(totalPortions / portionWeight)。
|
||||
/// 与后端 sumEnteredWeightByBillNos 同口径,确保离线场景也能正确显示。
|
||||
/// </summary>
|
||||
private void FillEnteredWeightFromLocalEntries(List<MesXslWeightRecord> records)
|
||||
{
|
||||
if (records.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var billNos = records
|
||||
.Select(r => r.BillNo)
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
if (billNos.Count == 0)
|
||||
{
|
||||
// 全部磅单都没有 BillNo:保留服务端返回值(若有),避免无端置 0。
|
||||
return;
|
||||
}
|
||||
var entries = _rawMaterialEntryService.GetCachedSnapshot();
|
||||
var sumMap = EnteredWeightCalculator.SumByBillNos(entries, billNos!);
|
||||
foreach (var r in records)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(r.BillNo))
|
||||
{
|
||||
r.EnteredWeight = 0d;
|
||||
continue;
|
||||
}
|
||||
r.EnteredWeight = sumMap.TryGetValue(r.BillNo, out var v) ? v : 0d;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<MesXslWeightRecord?> GetByIdAsync(string id, CancellationToken ct = default)
|
||||
{
|
||||
MesXslWeightRecord? record = null;
|
||||
if (_networkMonitor.IsOnline)
|
||||
{
|
||||
try
|
||||
@@ -119,11 +163,15 @@ public class WeightRecordService : IWeightRecordService, ISingletonDependency
|
||||
var url = $"{BaseUrl}/xslmes/mesXslWeightRecord/anon/queryById?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}";
|
||||
using var client = CreateClient();
|
||||
var resp = await client.GetAsync(url, ct).ConfigureAwait(false);
|
||||
if (!resp.IsSuccessStatusCode) return null;
|
||||
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (!doc.RootElement.TryGetProperty("result", out var resultEl)) return null;
|
||||
return resultEl.Deserialize<MesXslWeightRecord>(_jsonOpts);
|
||||
if (resp.IsSuccessStatusCode)
|
||||
{
|
||||
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (doc.RootElement.TryGetProperty("result", out var resultEl))
|
||||
{
|
||||
record = resultEl.Deserialize<MesXslWeightRecord>(_jsonOpts);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -131,11 +179,20 @@ public class WeightRecordService : IWeightRecordService, ISingletonDependency
|
||||
}
|
||||
}
|
||||
|
||||
lock (_cacheLock)
|
||||
if (record == null)
|
||||
{
|
||||
return _localCache.FirstOrDefault(v => string.Equals(v.Id, id, StringComparison.OrdinalIgnoreCase)) is { } found
|
||||
? Clone(found) : null;
|
||||
lock (_cacheLock)
|
||||
{
|
||||
record = _localCache.FirstOrDefault(v => string.Equals(v.Id, id, StringComparison.OrdinalIgnoreCase)) is { } found
|
||||
? Clone(found) : null;
|
||||
}
|
||||
}
|
||||
|
||||
if (record != null)
|
||||
{
|
||||
FillEnteredWeightFromLocalEntries(new List<MesXslWeightRecord> { record });
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
public async Task<bool> AddAsync(MesXslWeightRecord entity, CancellationToken ct = default)
|
||||
@@ -648,6 +705,8 @@ public class WeightRecordService : IWeightRecordService, ISingletonDependency
|
||||
GrossWeight = input.GrossWeight,
|
||||
TareWeight = input.TareWeight,
|
||||
NetWeight = input.NetWeight,
|
||||
// 「已入场重量」由实时聚合写入,Clone 也要原样传递,避免本地缓存 / Pending 重放后被抹掉
|
||||
EnteredWeight = input.EnteredWeight,
|
||||
DriverName = input.DriverName,
|
||||
DriverPhone = input.DriverPhone,
|
||||
BillType = input.BillType,
|
||||
|
||||
@@ -158,6 +158,10 @@ public class StompWebSocketService : ISignalRService
|
||||
await SendFrameAsync(
|
||||
BuildSubscribeFrame("sub-mes-raw-material-cards", "/topic/sync/mes-raw-material-cards"),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
// 库区数据变更:订阅 /topic/sync/mes-warehouse-areas
|
||||
await SendFrameAsync(
|
||||
BuildSubscribeFrame("sub-mes-warehouse-areas", "/topic/sync/mes-warehouse-areas"),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// 订阅服务端 PONG 回复(应用层假在线检测)
|
||||
await SendFrameAsync(
|
||||
|
||||
@@ -14,6 +14,7 @@ using YY.Admin.ViewModels.Vehicle;
|
||||
using YY.Admin.Views.Vehicle;
|
||||
using YY.Admin.Views.WeightRecord;
|
||||
using YY.Admin.Views.RawMaterialCard;
|
||||
using YY.Admin.Views.WarehouseArea;
|
||||
using YY.Admin.Views.RawMaterialEntry;
|
||||
|
||||
namespace YY.Admin
|
||||
@@ -78,6 +79,8 @@ namespace YY.Admin
|
||||
containerRegistry.RegisterForNavigation<RawMaterialEntryOperationView>();
|
||||
// 原材料卡片
|
||||
containerRegistry.RegisterForNavigation<RawMaterialCardListView>();
|
||||
// 库区管理
|
||||
containerRegistry.RegisterForNavigation<WarehouseAreaListView>();
|
||||
}
|
||||
}
|
||||
public class DialogWindow : Window, IDialogWindow
|
||||
|
||||
@@ -22,6 +22,8 @@ using YY.Admin.Services.Service.Supplier;
|
||||
using YY.Admin.Services.Service.RawMaterialCard;
|
||||
using YY.Admin.Services.Service.RawMaterialEntry;
|
||||
using YY.Admin.Services.Service.Vehicle;
|
||||
using YY.Admin.Services.Service.Warehouse;
|
||||
using YY.Admin.Services.Service.WarehouseArea;
|
||||
using YY.Admin.Services.Service.WeightRecord;
|
||||
|
||||
namespace YY.Admin.Module;
|
||||
@@ -61,6 +63,11 @@ public class SyncModule : IModule
|
||||
// 原材料卡片:免密 API 直连 + STOMP 实时通知
|
||||
containerRegistry.RegisterSingleton<IRawMaterialCardService, RawMaterialCardService>();
|
||||
containerRegistry.RegisterSingleton<RawMaterialCardSyncCoordinator>();
|
||||
// 仓库数据只读缓存(供库区等模块筛选使用,不含 CRUD 页面)
|
||||
containerRegistry.RegisterSingleton<IWarehouseService, WarehouseService>();
|
||||
// 库区管理:免密 API 直连 + STOMP 实时通知
|
||||
containerRegistry.RegisterSingleton<IWarehouseAreaService, WarehouseAreaService>();
|
||||
containerRegistry.RegisterSingleton<WarehouseAreaSyncCoordinator>();
|
||||
// 分类字典:启动同步 + 断线重连补刷
|
||||
containerRegistry.RegisterSingleton<CategorySyncCoordinator>();
|
||||
// 数据字典:启动同步 + 断线重连补刷
|
||||
@@ -127,6 +134,8 @@ public class SyncModule : IModule
|
||||
_ = containerProvider.Resolve<RawMaterialEntrySyncCoordinator>();
|
||||
// 强制实例化原材料卡片同步协调器
|
||||
_ = containerProvider.Resolve<RawMaterialCardSyncCoordinator>();
|
||||
// 强制实例化库区同步协调器
|
||||
_ = containerProvider.Resolve<WarehouseAreaSyncCoordinator>();
|
||||
// 强制实例化分类字典同步协调器
|
||||
_ = containerProvider.Resolve<CategorySyncCoordinator>();
|
||||
// 强制实例化数据字典同步协调器
|
||||
|
||||
@@ -135,7 +135,12 @@ namespace YY.Admin.ViewModels.Control
|
||||
// 已实现页面:原材料卡片
|
||||
["RawMaterialCardListView"] = "RawMaterialCardListView",
|
||||
["/xslmes/mesXslRawMaterialCard"] = "RawMaterialCardListView",
|
||||
["mesXslRawMaterialCard"] = "RawMaterialCardListView"
|
||||
["mesXslRawMaterialCard"] = "RawMaterialCardListView",
|
||||
|
||||
// 已实现页面:库区管理
|
||||
["WarehouseAreaListView"] = "WarehouseAreaListView",
|
||||
["/xslmes/mesXslWarehouseArea"] = "WarehouseAreaListView",
|
||||
["mesXslWarehouseArea"] = "WarehouseAreaListView"
|
||||
};
|
||||
|
||||
private MenuItem? _selectedMenuItem;
|
||||
|
||||
@@ -63,6 +63,9 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
|
||||
public bool IsAddMode => string.IsNullOrWhiteSpace(Entry?.Id);
|
||||
public string DialogTitle => IsAddMode ? "新增原料入场记录" : "编辑原料入场记录";
|
||||
/// <summary>已打印(PrintFlag=="1"):总重只读、不可新增明细。</summary>
|
||||
public bool IsPrinted => string.Equals(Entry?.PrintFlag, "1", StringComparison.Ordinal);
|
||||
public bool IsTotalWeightEditable => !IsPrinted;
|
||||
public string SelectedMaterialDisplay => _selectedMaterial == null
|
||||
? "请选择密炼物料"
|
||||
: $"[{_selectedMaterial.MaterialCode}] {_selectedMaterial.MaterialName}";
|
||||
@@ -130,6 +133,10 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
public DelegateCommand ClearWeightRecordCommand { get; }
|
||||
public DelegateCommand OpenSupplierPickerCommand { get; }
|
||||
public DelegateCommand ClearSupplierCommand { get; }
|
||||
/// <summary>
|
||||
/// 拆码明细 - 库位选择命令(弹出「库区选择」弹窗,单选)。CommandParameter 为当前行 RawMaterialSplitDetailItem。
|
||||
/// </summary>
|
||||
public DelegateCommand<RawMaterialSplitDetailItem> OpenWarehouseAreaPickerCommand { get; }
|
||||
|
||||
public RawMaterialEntryEditDialogViewModel(
|
||||
IRawMaterialEntryService entryService,
|
||||
@@ -144,7 +151,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
SaveCommand = new DelegateCommand(async () => await SaveAsync());
|
||||
CancelCommand = new DelegateCommand(() => CloseAction?.Invoke());
|
||||
ResetCommand = new DelegateCommand(InitializeForAdd);
|
||||
AddSplitDetailCommand = new DelegateCommand(AddSplitDetailRow);
|
||||
AddSplitDetailCommand = new DelegateCommand(AddSplitDetailRow, () => !IsPrinted);
|
||||
RemoveSplitDetailCommand = new DelegateCommand<RawMaterialSplitDetailItem>(RemoveSplitDetailRow);
|
||||
OpenMaterialPickerCommand = new DelegateCommand(async () => await OpenMaterialPickerAsync());
|
||||
ClearMaterialCommand = new DelegateCommand(ClearMaterialSelection);
|
||||
@@ -152,6 +159,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
ClearWeightRecordCommand = new DelegateCommand(ClearWeightRecordSelection);
|
||||
OpenSupplierPickerCommand = new DelegateCommand(async () => await OpenSupplierPickerAsync());
|
||||
ClearSupplierCommand = new DelegateCommand(ClearSupplierSelection);
|
||||
OpenWarehouseAreaPickerCommand = new DelegateCommand<RawMaterialSplitDetailItem>(async row => await OpenWarehouseAreaPickerAsync(row));
|
||||
SplitCodeDetails.CollectionChanged += OnSplitCodeDetailsCollectionChanged;
|
||||
_ = LoadAllAsync();
|
||||
}
|
||||
@@ -304,6 +312,9 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
RaisePropertyChanged(nameof(SplitTotalPortionsDisplay));
|
||||
RaisePropertyChanged(nameof(SplitPortionWeightDisplay));
|
||||
RaisePropertyChanged(nameof(SplitPortionPackagesDisplay));
|
||||
RaisePropertyChanged(nameof(IsPrinted));
|
||||
RaisePropertyChanged(nameof(IsTotalWeightEditable));
|
||||
AddSplitDetailCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
public void InitializeForEdit(MesXslRawMaterialEntry entry)
|
||||
@@ -317,6 +328,9 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
ManufacturerMaterialName = entry.ManufacturerMaterialName,
|
||||
ShelfLife = entry.ShelfLife, TotalWeight = entry.TotalWeight, TotalPortions = entry.TotalPortions,
|
||||
PortionWeight = entry.PortionWeight, PortionPackages = entry.PortionPackages,
|
||||
PortionWarehouseLocations = entry.PortionWarehouseLocations,
|
||||
PortionDetailIds = entry.PortionDetailIds,
|
||||
PortionCardFlags = entry.PortionCardFlags,
|
||||
TestResult = entry.TestResult, TestStatus = entry.TestStatus, PrintFlag = entry.PrintFlag,
|
||||
StockBalance = entry.StockBalance, WarehouseLocation = entry.WarehouseLocation,
|
||||
UnloadOperator = entry.UnloadOperator, IsSpecialAdoption = entry.IsSpecialAdoption,
|
||||
@@ -347,6 +361,9 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
RaisePropertyChanged(nameof(SplitTotalPortionsDisplay));
|
||||
RaisePropertyChanged(nameof(SplitPortionWeightDisplay));
|
||||
RaisePropertyChanged(nameof(SplitPortionPackagesDisplay));
|
||||
RaisePropertyChanged(nameof(IsPrinted));
|
||||
RaisePropertyChanged(nameof(IsTotalWeightEditable));
|
||||
AddSplitDetailCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
protected virtual async Task SaveAsync()
|
||||
@@ -357,6 +374,18 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
ApplySplitDetailsToEntry();
|
||||
ApplyDefaultEntryStatusForAdd();
|
||||
ApplyHiddenFieldDefaultsForAdd();
|
||||
|
||||
var missing = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(Entry.MaterialId)) missing.Add("密炼物料");
|
||||
if (string.IsNullOrWhiteSpace(Entry.BillNo)) missing.Add("榜单号");
|
||||
if (string.IsNullOrWhiteSpace(Entry.UnloadOperator)) missing.Add("卸货人");
|
||||
if (string.IsNullOrWhiteSpace(Entry.SupplierName)) missing.Add("供应商名称");
|
||||
if (missing.Count > 0)
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Warning($"以下必填项不能为空:{string.Join("、", missing)}");
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok;
|
||||
if (IsAddMode)
|
||||
{
|
||||
@@ -414,7 +443,57 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
Entry.BillNo = selected.BillNo;
|
||||
Entry.SupplierName = selected.SenderUnit;
|
||||
Entry.SupplierId = null;
|
||||
// 选择榜单后,自动把「剩余可入场量 = 净重 - 已入场重量」带入「总重」,
|
||||
// 用户仍可手动编辑;若净重为空则保留原值,避免误清空。
|
||||
if (selected.NetWeight.HasValue)
|
||||
{
|
||||
var entered = selected.EnteredWeight ?? 0d;
|
||||
var remaining = selected.NetWeight.Value - entered;
|
||||
// 已入场重量可能因数据异常超过净重;UI 不展示负数,强制夹到 0。
|
||||
Entry.TotalWeight = Math.Round(Math.Max(0d, remaining), 2);
|
||||
}
|
||||
RaisePropertyChanged(nameof(Entry));
|
||||
RaisePropertyChanged(nameof(TotalWeightInput));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 弹出「库区选择」弹窗,把选中的 AreaName 写回当前明细行的 WarehouseLocation。
|
||||
/// 入参 row:被点击的拆码明细行;为 null 直接 return(防御性)。
|
||||
/// </summary>
|
||||
private async Task OpenWarehouseAreaPickerAsync(RawMaterialSplitDetailItem? row)
|
||||
{
|
||||
if (row == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
WarehouseAreaPickerDialogViewModel? pickerVm = null;
|
||||
bool confirmed;
|
||||
try
|
||||
{
|
||||
confirmed = await HandyControl.Controls.Dialog.Show<WarehouseAreaPickerDialogView>()
|
||||
.Initialize<WarehouseAreaPickerDialogViewModel>(vm =>
|
||||
{
|
||||
pickerVm = vm;
|
||||
vm.Initialize(row.WarehouseLocation);
|
||||
})
|
||||
.GetResultAsync<bool>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 保留 Debug 输出以便排查,但不阻断流程
|
||||
System.Diagnostics.Debug.WriteLine($"[库位选择] 弹窗异常: {ex}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirmed || pickerVm?.SelectedRecord == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 仅回填库区名称(与后端 warehouse_location 字段保持单字符串语义)。
|
||||
// 如未来需要严格关联库区 ID,可在 RawMaterialSplitDetailItem 新增 WarehouseAreaId 一并保存。
|
||||
row.WarehouseLocation = pickerVm.SelectedRecord.AreaName;
|
||||
}
|
||||
|
||||
private async Task OpenMaterialPickerAsync()
|
||||
@@ -537,7 +616,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
|
||||
/// <summary>
|
||||
/// 新增模式下为已在前端隐藏的字段补充默认值,确保保存到后端时不为空:
|
||||
/// 检测结果=未检、检测状态=送样、打印标记=未打印、入库结存=否。
|
||||
/// 检测结果=未检、打印标记=未打印、入库结存=否。
|
||||
/// 字典就绪时取字典 code,未就绪时回退到约定的常用 code。
|
||||
/// </summary>
|
||||
protected void ApplyHiddenFieldDefaultsForAdd()
|
||||
@@ -551,10 +630,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
{
|
||||
Entry.TestResult = ResolveDefaultOptionValue(TestResultOptions, "未检", "0");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(Entry.TestStatus))
|
||||
{
|
||||
Entry.TestStatus = ResolveDefaultOptionValue(TestStatusOptions, "送样", "0");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Entry.PrintFlag))
|
||||
{
|
||||
Entry.PrintFlag = ResolveDefaultOptionValue(PrintFlagOptions, "未打印", "0");
|
||||
@@ -582,22 +658,58 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
{
|
||||
SplitCodeDetails.Clear();
|
||||
|
||||
// 三个字段是按拆码明细多行拼接的字符串(末尾带 /),解析回明细列表
|
||||
// 六个字段都是按拆码明细多行拼接的字符串(末尾带 /),解析回明细列表
|
||||
var portionsArr = SplitJoinedValues(Entry?.TotalPortions);
|
||||
var weightArr = SplitJoinedValues(Entry?.PortionWeight);
|
||||
var packagesArr = SplitJoinedValues(Entry?.PortionPackages);
|
||||
var rowCount = Math.Max(1, Math.Max(portionsArr.Length, Math.Max(weightArr.Length, packagesArr.Length)));
|
||||
var locationsArr = SplitJoinedValues(Entry?.PortionWarehouseLocations);
|
||||
var idsArr = SplitJoinedValues(Entry?.PortionDetailIds);
|
||||
var cardFlagsArr = SplitJoinedValues(Entry?.PortionCardFlags);
|
||||
// 历史记录可能没存 PortionCardFlags:用 print_flag 推断(与原行为兼容)
|
||||
var fallbackToPrintFlag = cardFlagsArr.Length == 0
|
||||
&& string.Equals(Entry?.PrintFlag, "1", StringComparison.Ordinal);
|
||||
var rowCount = Math.Max(1, Math.Max(Math.Max(portionsArr.Length, weightArr.Length),
|
||||
Math.Max(Math.Max(packagesArr.Length, locationsArr.Length), idsArr.Length)));
|
||||
|
||||
for (var i = 0; i < rowCount; i++)
|
||||
{
|
||||
SplitCodeDetails.Add(new RawMaterialSplitDetailItem
|
||||
var locationFromArr = GetAt(locationsArr, i);
|
||||
// 兼容历史数据:明细库位拼接字段为空时,首行回退到 Entry.WarehouseLocation
|
||||
// (早期版本里整票级库位曾被反写过,避免老记录打开后明细全空)
|
||||
var locationFallback = (i == 0 && string.IsNullOrWhiteSpace(locationFromArr))
|
||||
? Entry?.WarehouseLocation
|
||||
: locationFromArr;
|
||||
|
||||
var item = new RawMaterialSplitDetailItem
|
||||
{
|
||||
Portions = TryParseInt(GetAt(portionsArr, i)),
|
||||
PortionWeight = TryParseDouble(GetAt(weightArr, i)),
|
||||
PortionPackages = TryParseInt(GetAt(packagesArr, i)),
|
||||
// 库位是单值字段,仅赋给首行
|
||||
WarehouseLocation = i == 0 ? Entry?.WarehouseLocation : null,
|
||||
});
|
||||
WarehouseLocation = locationFallback,
|
||||
};
|
||||
// 仅当后端已持久化了该行 ID 时才覆盖默认值,确保跨次保持稳定;
|
||||
// 历史记录无 ID 字段时使用构造期生成的新 GUID(之后保存会回填)。
|
||||
var existingId = GetAt(idsArr, i);
|
||||
if (!string.IsNullOrWhiteSpace(existingId))
|
||||
{
|
||||
item.Id = existingId;
|
||||
}
|
||||
// HasCard 行级解析(优先级 1):从 PortionCardFlags 按位读取,"1"=已生成。
|
||||
// 这是「保存后新增未生成行不被误判为已打印」的关键 — 新增行保存时 HasCard=false 持久化为 "0",
|
||||
// 重新加载时回填 false,「生成原材料卡片」就能正确把它列入待生成清单。
|
||||
var flagAt = GetAt(cardFlagsArr, i);
|
||||
if (!string.IsNullOrWhiteSpace(flagAt))
|
||||
{
|
||||
item.HasCard = string.Equals(flagAt, "1", StringComparison.Ordinal);
|
||||
}
|
||||
else if (fallbackToPrintFlag && !string.IsNullOrWhiteSpace(existingId))
|
||||
{
|
||||
// 优先级 2(兼容历史记录):未存 PortionCardFlags 时,沿用旧逻辑——
|
||||
// 持久化 ID 存在 + 整票已打印 ⇒ 视为已生成卡片。
|
||||
item.HasCard = true;
|
||||
}
|
||||
// 否则保持构造默认值 false(新增态、未打印整票等场景)
|
||||
SplitCodeDetails.Add(item);
|
||||
}
|
||||
|
||||
RaisePropertyChanged(nameof(SplitCodeTableHeight));
|
||||
@@ -666,6 +778,14 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
|
||||
private void OnSplitDetailItemPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
// 用户填写或修改某行的「份数」时,按公式自动算出该行「每份重量」:
|
||||
// 每份重量 = (总重 - Σ其他行的份数×每份重量) / 当前行份数
|
||||
if (e.PropertyName == nameof(RawMaterialSplitDetailItem.Portions)
|
||||
&& sender is RawMaterialSplitDetailItem row)
|
||||
{
|
||||
RecalculatePortionWeightForRow(row);
|
||||
}
|
||||
|
||||
if (e.PropertyName is nameof(RawMaterialSplitDetailItem.Portions)
|
||||
or nameof(RawMaterialSplitDetailItem.PortionWeight)
|
||||
or nameof(RawMaterialSplitDetailItem.PortionPackages))
|
||||
@@ -674,6 +794,42 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 拆码明细某行的「份数」变化时,按公式重算该行「每份重量」:
|
||||
/// 公式:每份重量 = (总重 - 其他行 Σ份数×每份重量) / 当前行份数
|
||||
/// — 用户单独手改「每份重量」不触发;
|
||||
/// — 总重为空 / ≤0、当前份数 ≤0、剩余总重 ≤0 时跳过;
|
||||
/// — 结果四舍五入到两位小数;
|
||||
/// — 写入 row.PortionWeight 会触发 PropertyChanged(PortionWeight),不会再次进入本方法(PropertyName 已不是 Portions),不会形成循环。
|
||||
/// </summary>
|
||||
private void RecalculatePortionWeightForRow(RawMaterialSplitDetailItem row)
|
||||
{
|
||||
if (Entry?.TotalWeight is not { } total || total <= 0d)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (row.Portions is not { } portions || portions <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var otherSum = 0d;
|
||||
foreach (var other in SplitCodeDetails)
|
||||
{
|
||||
if (ReferenceEquals(other, row)) continue;
|
||||
if (other.Portions is not { } op || other.PortionWeight is not { } ow) continue;
|
||||
otherSum += op * ow;
|
||||
}
|
||||
|
||||
var remaining = total - otherSum;
|
||||
if (remaining <= 0d)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
row.PortionWeight = Math.Round(remaining / portions, 2);
|
||||
}
|
||||
|
||||
private void RaiseSplitDisplayPropertyChanged()
|
||||
{
|
||||
RaisePropertyChanged(nameof(SplitTotalPortionsDisplay));
|
||||
@@ -708,21 +864,26 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把 SplitCodeDetails 全部明细行的「份数 / 每份重量 / 每份包数」按 "x/y/z/" 拼接后持久化到 Entry,
|
||||
/// 库位仍取首行(业务上库位是单值)。与 SplitTotalPortionsDisplay 等只读展示属性同规则。
|
||||
/// 把 SplitCodeDetails 全部明细行的「份数 / 每份重量 / 每份包数 / 库位 / 行 ID」按 "x/y/z/" 拼接后持久化到 Entry。
|
||||
/// 明细行的「库位」属于行级数据(用于后续生成原材料卡片时区分库位),与 Entry.WarehouseLocation
|
||||
/// (基础资料整票级单值)独立保存,通过 portion_warehouse_locations 字段持久化。
|
||||
/// 子类(如 OperationViewModel 的「重新拆码」)需要直接调用,因此声明为 protected。
|
||||
/// </summary>
|
||||
private void ApplySplitDetailsToEntry()
|
||||
protected void ApplySplitDetailsToEntry()
|
||||
{
|
||||
if (Entry == null) return;
|
||||
|
||||
Entry.TotalPortions = JoinSplitValue(it => it.Portions?.ToString(CultureInfo.InvariantCulture), true);
|
||||
Entry.PortionWeight = JoinSplitValue(it => FormatNullableDecimal(it.PortionWeight), true);
|
||||
Entry.PortionPackages = JoinSplitValue(it => it.PortionPackages?.ToString(CultureInfo.InvariantCulture), true);
|
||||
|
||||
if (SplitCodeDetails.Count > 0)
|
||||
{
|
||||
Entry.WarehouseLocation = SplitCodeDetails[0].WarehouseLocation;
|
||||
}
|
||||
Entry.PortionWarehouseLocations = JoinSplitValue(it => string.IsNullOrWhiteSpace(it.WarehouseLocation) ? null : it.WarehouseLocation.Trim(), true);
|
||||
// 持久化每行 GUID,行序与上方四个字段对齐;JoinSplitValue 会过滤空,但 Id 在构造时即生成不会为空。
|
||||
Entry.PortionDetailIds = JoinSplitValue(it => it.Id, true);
|
||||
// 持久化每行的「已生成卡片」标志(1=已生成 0=未生成),
|
||||
// 用于解决「保存后新增行被误判为已打印」的问题:
|
||||
// 「生成原材料卡片」只对 HasCard=false 的行加卡 → 必须把真实的 HasCard 持久化,
|
||||
// 否则重新加载后无法区分「已生成」与「保存后新增的未生成行」。
|
||||
Entry.PortionCardFlags = JoinSplitValue(it => it.HasCard ? "1" : "0", true);
|
||||
}
|
||||
|
||||
private double CalculateSplitCodeTableHeight()
|
||||
@@ -740,6 +901,14 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
|
||||
public class RawMaterialSplitDetailItem : BindableBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 拆码明细行 ID(GUID,构造时生成)。前端隐藏;
|
||||
/// 「生成原材料卡片」时由桌面端写入对应 MesXslRawMaterialCard.SplitDetailId;
|
||||
/// 「重新拆码」时按入场记录的 PortionDetailIds 反查批删关联卡片。
|
||||
/// 编辑回填时若 portion_detail_ids 已有该位置的值,会覆盖默认值,确保跨次保持稳定。
|
||||
/// </summary>
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||
|
||||
private int? _portions;
|
||||
public int? Portions
|
||||
{
|
||||
@@ -767,4 +936,18 @@ public class RawMaterialSplitDetailItem : BindableBase
|
||||
get => _warehouseLocation;
|
||||
set => SetProperty(ref _warehouseLocation, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 该行是否已生成原材料卡片(运行时状态,不直接持久化)。
|
||||
/// 加载时根据 Entry.PrintFlag + Id 是否来自持久化的 PortionDetailIds 推断;
|
||||
/// 「生成原材料卡片」成功后由 VM 置为 true;「重新拆码」清空集合时随之失效。
|
||||
/// true → 行内输入项 / 删除 全部锁定,避免与已生成卡片数据脱节;
|
||||
/// false → 该行参与下次「生成原材料卡片」(继续拆码 + 续号生成)。
|
||||
/// </summary>
|
||||
private bool _hasCard;
|
||||
public bool HasCard
|
||||
{
|
||||
get => _hasCard;
|
||||
set => SetProperty(ref _hasCard, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,20 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
||||
|
||||
public ObservableCollection<MesXslRawMaterialEntry> TodayEntries { get; } = new();
|
||||
|
||||
public IReadOnlyList<string> DateRangeOptions { get; } =
|
||||
["今日", "过去24小时", "过去48小时", "过去72小时"];
|
||||
|
||||
private string _selectedDateRange = "今日";
|
||||
public string SelectedDateRange
|
||||
{
|
||||
get => _selectedDateRange;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedDateRange, value))
|
||||
_ = LoadTodayEntriesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private MesXslRawMaterialEntry? _selectedTodayEntry;
|
||||
public MesXslRawMaterialEntry? SelectedTodayEntry
|
||||
{
|
||||
@@ -67,12 +81,20 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>仅当入场记录已保存(有 Id)且存在拆码明细时允许生成原材料卡片。</summary>
|
||||
public bool CanGenerateCards => !string.IsNullOrWhiteSpace(Entry?.Id) && SplitCodeDetails.Count > 0;
|
||||
/// <summary>
|
||||
/// 「生成原材料卡片」按钮可用条件:
|
||||
/// 入场记录已保存(有 Id)且至少存在一行「未生成卡片 + 份数>0」的明细。
|
||||
/// 已打印态下用户「继续拆码」新增了行,按钮自动重新可用;全部行都已生成卡片时不可用。
|
||||
/// </summary>
|
||||
public bool CanGenerateCards =>
|
||||
!string.IsNullOrWhiteSpace(Entry?.Id)
|
||||
&& SplitCodeDetails.Any(d => !d.HasCard && (d.Portions ?? 0) > 0);
|
||||
|
||||
public DelegateCommand ToggleRightPanelCommand { get; }
|
||||
public DelegateCommand RefreshTodayEntriesCommand { get; }
|
||||
public DelegateCommand GenerateRawMaterialCardsCommand { get; }
|
||||
/// <summary>「重新拆码」:清除已生成的卡片 + 清空明细,仅在编辑态可用。</summary>
|
||||
public DelegateCommand ResplitCommand { get; }
|
||||
|
||||
public RawMaterialEntryOperationViewModel(
|
||||
IRawMaterialEntryService entryService,
|
||||
@@ -88,7 +110,52 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
||||
ToggleRightPanelCommand = new DelegateCommand(() => IsRightPanelExpanded = !IsRightPanelExpanded);
|
||||
RefreshTodayEntriesCommand = new DelegateCommand(async () => await LoadTodayEntriesAsync());
|
||||
GenerateRawMaterialCardsCommand = new DelegateCommand(async () => await GenerateRawMaterialCardsAsync());
|
||||
SplitCodeDetails.CollectionChanged += (_, _) => RaisePropertyChanged(nameof(CanGenerateCards));
|
||||
ResplitCommand = new DelegateCommand(async () => await ResplitAsync(), () => CanResplit)
|
||||
.ObservesProperty(() => Entry);
|
||||
// 集合变化:批量重订阅 item.PropertyChanged 监听 HasCard/Portions,并同步刷新两个 Can*。
|
||||
SplitCodeDetails.CollectionChanged += OnSplitCodeDetailsCollectionChangedForCanFlags;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 「重新拆码」按钮可用条件:入场记录已保存 + 至少有一行已生成卡片。
|
||||
/// 全是新增未生成的行时,用户直接点「删除」即可,无需走「清空卡片」流程。
|
||||
/// </summary>
|
||||
public bool CanResplit =>
|
||||
!string.IsNullOrWhiteSpace(Entry?.Id)
|
||||
&& SplitCodeDetails.Any(d => d.HasCard);
|
||||
|
||||
/// <summary>
|
||||
/// 集合变化时:对新增/移除的 item 维护 PropertyChanged 订阅,并刷新两个 Can* 属性。
|
||||
/// HasCard / Portions 变化会让「生成原材料卡片」和「重新拆码」按钮可用性同步刷新。
|
||||
/// </summary>
|
||||
private void OnSplitCodeDetailsCollectionChangedForCanFlags(
|
||||
object? sender,
|
||||
System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (e.OldItems != null)
|
||||
{
|
||||
foreach (RawMaterialSplitDetailItem o in e.OldItems)
|
||||
o.PropertyChanged -= OnSplitDetailHasCardChanged;
|
||||
}
|
||||
if (e.NewItems != null)
|
||||
{
|
||||
foreach (RawMaterialSplitDetailItem n in e.NewItems)
|
||||
n.PropertyChanged += OnSplitDetailHasCardChanged;
|
||||
}
|
||||
RaisePropertyChanged(nameof(CanGenerateCards));
|
||||
RaisePropertyChanged(nameof(CanResplit));
|
||||
ResplitCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
private void OnSplitDetailHasCardChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName is nameof(RawMaterialSplitDetailItem.HasCard)
|
||||
or nameof(RawMaterialSplitDetailItem.Portions))
|
||||
{
|
||||
RaisePropertyChanged(nameof(CanGenerateCards));
|
||||
RaisePropertyChanged(nameof(CanResplit));
|
||||
ResplitCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public override void InitializeForAdd()
|
||||
@@ -117,9 +184,15 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
||||
{
|
||||
IsLoading = true;
|
||||
var result = await EntryService.PageAsync(1, TodayListFetchSize);
|
||||
var today = DateTime.Today;
|
||||
DateTime? threshold = _selectedDateRange switch
|
||||
{
|
||||
"过去24小时" => DateTime.Now.AddHours(-24),
|
||||
"过去48小时" => DateTime.Now.AddHours(-48),
|
||||
"过去72小时" => DateTime.Now.AddHours(-72),
|
||||
_ => null // "今日":按自然日过滤
|
||||
};
|
||||
var rows = result.Records
|
||||
.Where(e => IsTodayEntry(e, today))
|
||||
.Where(e => IsInRange(e, threshold))
|
||||
.OrderByDescending(e => e.EntryTime ?? e.CreateTime ?? DateTime.MinValue)
|
||||
.ToList();
|
||||
TodayEntries.Clear();
|
||||
@@ -136,11 +209,14 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsTodayEntry(MesXslRawMaterialEntry e, DateTime today)
|
||||
private static bool IsInRange(MesXslRawMaterialEntry e, DateTime? threshold)
|
||||
{
|
||||
var byEntry = e.EntryTime?.Date == today;
|
||||
var byCreate = e.CreateTime?.Date == today;
|
||||
return byEntry || byCreate;
|
||||
if (threshold == null)
|
||||
{
|
||||
var today = DateTime.Today;
|
||||
return (e.EntryTime?.Date == today) || (e.CreateTime?.Date == today);
|
||||
}
|
||||
return (e.EntryTime >= threshold) || (e.CreateTime >= threshold);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -158,20 +234,118 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
||||
await LoadMaterialOptionsAsync();
|
||||
}
|
||||
|
||||
/// <summary>编辑保存成功后:刷新右侧今日列表并切回新增态,避免连续误改同一条。</summary>
|
||||
/// <summary>
|
||||
/// 保存成功后:无论新增还是编辑,都立即刷新右侧「今日入场」列表。
|
||||
/// 编辑成功额外把表单切回「新增态」,避免用户连续误改同一条;新增成功后由基类负责清空表单。
|
||||
/// </summary>
|
||||
protected override async Task SaveAsync()
|
||||
{
|
||||
var wasEdit = !IsAddMode;
|
||||
await base.SaveAsync();
|
||||
RaisePropertyChanged(nameof(CanGenerateCards));
|
||||
if (wasEdit && Result && CloseAction == null)
|
||||
|
||||
if (!Result || CloseAction != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await LoadTodayEntriesAsync();
|
||||
|
||||
if (wasEdit)
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Success("编辑成功!");
|
||||
await LoadTodayEntriesAsync();
|
||||
InitializeForAdd();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 「重新拆码」:弹窗预提示「将清除 N 张原材料卡片」→ 确认 → 后端按 splitDetailId IN 批删
|
||||
/// → 清空 SplitCodeDetails → Entry.PrintFlag=0 → EditAsync(Entry) 持久化新状态 → 刷新今日列表。
|
||||
/// 流程对应「清空卡片重新生成」的业务诉求;用户可立即重新维护明细并再次「生成原材料卡片」。
|
||||
/// </summary>
|
||||
private async Task ResplitAsync()
|
||||
{
|
||||
if (Entry == null || string.IsNullOrWhiteSpace(Entry.Id))
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Warning("请先选中一条已保存的入场记录后再「重新拆码」。");
|
||||
return;
|
||||
}
|
||||
var detailIds = SplitCodeDetails
|
||||
.Select(d => d.Id)
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
|
||||
// dryRun 预查:统计当前明细行已生成的卡片数量
|
||||
var cardCount = detailIds.Count > 0
|
||||
? await _rawMaterialCardService.DeleteBySplitDetailIdsAsync(detailIds, dryRun: true)
|
||||
: 0;
|
||||
if (cardCount < 0)
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Error("无法连接服务器,重新拆码已取消。");
|
||||
return;
|
||||
}
|
||||
|
||||
var confirm = HandyControl.Controls.MessageBox.Show(
|
||||
$"当前入场记录已生成 {cardCount} 条原材料卡片,是否清空卡片重新生成?",
|
||||
"重新拆码",
|
||||
System.Windows.MessageBoxButton.OKCancel,
|
||||
System.Windows.MessageBoxImage.Question);
|
||||
if (confirm != System.Windows.MessageBoxResult.OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 真正执行批删(即使 cardCount=0 也调用一次,结果幂等)
|
||||
if (cardCount > 0)
|
||||
{
|
||||
var deleted = await _rawMaterialCardService.DeleteBySplitDetailIdsAsync(detailIds, dryRun: false);
|
||||
if (deleted < 0)
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Error("清除已生成的原材料卡片失败,重新拆码已中止。");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 清空明细 + 重置打印标记,并把变更持久化到后端入场记录
|
||||
SplitCodeDetails.Clear();
|
||||
Entry.PrintFlag = "0";
|
||||
ApplySplitDetailsToEntry(); // 同步把 PortionDetailIds 等清空到 Entry 上
|
||||
var ok = await EntryService.EditAsync(Entry);
|
||||
if (!ok)
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Error("保存重新拆码后的入场记录失败,请刷新后重试。");
|
||||
return;
|
||||
}
|
||||
|
||||
RaisePropertyChanged(nameof(Entry));
|
||||
RaisePropertyChanged(nameof(IsPrinted));
|
||||
RaisePropertyChanged(nameof(IsTotalWeightEditable));
|
||||
AddSplitDetailCommand.RaiseCanExecuteChanged();
|
||||
RaisePropertyChanged(nameof(CanGenerateCards));
|
||||
RaisePropertyChanged(nameof(CanResplit));
|
||||
HandyControl.Controls.MessageBox.Success($"已清除 {cardCount} 条原材料卡片,请重新维护拆码明细。");
|
||||
await LoadTodayEntriesAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Error($"重新拆码失败:{ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 「生成原材料卡片」:仅处理 HasCard==false 的明细行(「继续拆码」流程的核心)。
|
||||
/// 条码续号 = 已有卡片总数(Σ HasCard==true 行的 Portions)+ 1,避免与已生成卡片的条码冲突。
|
||||
/// 成功生成的行置 HasCard=true,Entry.PrintFlag=1(整票级标记)。
|
||||
/// </summary>
|
||||
private async Task GenerateRawMaterialCardsAsync()
|
||||
{
|
||||
if (Entry == null || string.IsNullOrWhiteSpace(Entry.Id))
|
||||
@@ -179,26 +353,73 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
||||
HandyControl.Controls.MessageBox.Warning("请先保存入场记录再生成原材料卡片!");
|
||||
return;
|
||||
}
|
||||
if (SplitCodeDetails.Count == 0)
|
||||
|
||||
// 只处理未生成卡片的行 + 份数>0;已生成行直接跳过,避免重复加卡和条码冲突
|
||||
var pendingRows = SplitCodeDetails
|
||||
.Where(d => !d.HasCard && (d.Portions ?? 0) > 0)
|
||||
.ToList();
|
||||
if (pendingRows.Count == 0)
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Warning("当前入场记录无拆分明细,无法生成原材料卡片!");
|
||||
HandyControl.Controls.MessageBox.Warning("当前没有待生成卡片的拆码明细(已生成的行不会重复处理)。请先「新增明细」再生成。");
|
||||
return;
|
||||
}
|
||||
|
||||
// 库位必填仅校验本批待生成行;行号取在原集合的真实索引,避免编号偏移误导
|
||||
foreach (var row in pendingRows)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(row.WarehouseLocation))
|
||||
{
|
||||
var realIndex = SplitCodeDetails.IndexOf(row);
|
||||
HandyControl.Controls.MessageBox.Warning($"拆码明细第 {realIndex + 1} 行的「库位」未填写,请点击该行库位选择库区后再生成原材料卡片。");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 总重核对:拆码明细合计 vs 基础资料总重
|
||||
{
|
||||
var splitTotal = SplitCodeDetails.Sum(d => (d.PortionWeight ?? 0d) * (d.Portions ?? 0));
|
||||
var basicTotal = Entry.TotalWeight ?? 0d;
|
||||
if (Math.Abs(splitTotal - basicTotal) > 0.01d)
|
||||
{
|
||||
string hint;
|
||||
if (splitTotal > basicTotal)
|
||||
hint = $"本次打印拆码总重 {splitTotal:0.##},超出基础资料总重 {basicTotal:0.##},超出部分是否需要核对?";
|
||||
else
|
||||
hint = $"本次打印拆码总重 {splitTotal:0.##},少于基础资料总重 {basicTotal:0.##},剩余部分将无法继续拆码,是否继续?";
|
||||
var confirm = System.Windows.MessageBox.Show(
|
||||
hint,
|
||||
"总重核对",
|
||||
System.Windows.MessageBoxButton.OKCancel,
|
||||
System.Windows.MessageBoxImage.Warning);
|
||||
if (confirm != System.Windows.MessageBoxResult.OK) return;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
var globalIndex = 1;
|
||||
// 续号起点:已有卡片数 = Σ HasCard==true 行的 Portions;新增卡片从其后续编
|
||||
var alreadyGenerated = SplitCodeDetails
|
||||
.Where(d => d.HasCard)
|
||||
.Sum(d => d.Portions ?? 0);
|
||||
var globalIndex = alreadyGenerated + 1;
|
||||
var baseBarcode = Entry.Barcode ?? "";
|
||||
var failCount = 0;
|
||||
var newCardCount = 0;
|
||||
// 按行收集成功标记:只要该行所有份都加卡成功,行 HasCard=true;
|
||||
// 中途任一份失败则保留 HasCard=false,下次再点「生成」时会重试该行
|
||||
var rowSuccessMap = new Dictionary<RawMaterialSplitDetailItem, bool>();
|
||||
|
||||
foreach (var detail in SplitCodeDetails)
|
||||
foreach (var detail in pendingRows)
|
||||
{
|
||||
var portions = detail.Portions ?? 0;
|
||||
var rowAllOk = true;
|
||||
for (var i = 0; i < portions; i++)
|
||||
{
|
||||
var card = new MesXslRawMaterialCard
|
||||
{
|
||||
// 关联到当前拆码明细行 — 便于「重新拆码」按此 ID 批删
|
||||
SplitDetailId = detail.Id,
|
||||
Barcode = baseBarcode + globalIndex.ToString("D3"),
|
||||
BatchNo = Entry.BatchNo,
|
||||
EntryDate = Entry.EntryTime?.Date ?? DateTime.Today,
|
||||
@@ -219,21 +440,39 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
||||
TenantId = Entry.TenantId
|
||||
};
|
||||
var ok = await _rawMaterialCardService.AddAsync(card);
|
||||
if (!ok) failCount++;
|
||||
if (ok) newCardCount++;
|
||||
else { failCount++; rowAllOk = false; }
|
||||
globalIndex++;
|
||||
}
|
||||
rowSuccessMap[detail] = rowAllOk;
|
||||
}
|
||||
|
||||
// 更新打印状态为「已打印」
|
||||
Entry.PrintFlag = "1";
|
||||
await EntryService.EditAsync(Entry);
|
||||
RaisePropertyChanged(nameof(Entry));
|
||||
// 把整行加卡都成功的标记 HasCard=true(触发该行 UI 自动锁定 + 删除按钮隐藏 + 打印列显示「已打印」)
|
||||
foreach (var kv in rowSuccessMap)
|
||||
{
|
||||
if (kv.Value) kv.Key.HasCard = true;
|
||||
}
|
||||
|
||||
// 整票级 PrintFlag:只要本批至少有一张卡片成功即置 1(与原行为兼容)。
|
||||
// 关键:必须先 ApplySplitDetailsToEntry(),把刚被设为 true 的 HasCard 序列化到 PortionCardFlags,
|
||||
// 否则下次重新打开时该行会回退为 false(PortionCardFlags 旧值未更新),从而被「继续拆码」流程误纳入待生成。
|
||||
if (newCardCount > 0)
|
||||
{
|
||||
Entry.PrintFlag = "1";
|
||||
ApplySplitDetailsToEntry();
|
||||
await EntryService.EditAsync(Entry);
|
||||
RaisePropertyChanged(nameof(Entry));
|
||||
RaisePropertyChanged(nameof(IsPrinted));
|
||||
RaisePropertyChanged(nameof(IsTotalWeightEditable));
|
||||
AddSplitDetailCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
RaisePropertyChanged(nameof(CanGenerateCards));
|
||||
RaisePropertyChanged(nameof(CanResplit));
|
||||
|
||||
var total = globalIndex - 1;
|
||||
if (failCount == 0)
|
||||
HandyControl.Controls.MessageBox.Success($"已生成 {total} 张原材料卡片,打印状态已更新为「已打印」!");
|
||||
HandyControl.Controls.MessageBox.Success($"已生成 {newCardCount} 张原材料卡片(累计 {alreadyGenerated + newCardCount} 张),打印状态已更新为「已打印」!");
|
||||
else
|
||||
HandyControl.Controls.MessageBox.Warning($"共生成 {total} 张,其中 {failCount} 张失败,请检查网络后重试!");
|
||||
HandyControl.Controls.MessageBox.Warning($"本次共尝试生成 {newCardCount + failCount} 张,成功 {newCardCount} 张,失败 {failCount} 张。失败的行未标记为「已打印」,可检查网络后再次点击「生成原材料卡片」重试。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
using HandyControl.Tools.Extension;
|
||||
using System.Collections.ObjectModel;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Services;
|
||||
|
||||
namespace YY.Admin.ViewModels.RawMaterialEntry;
|
||||
|
||||
/// <summary>
|
||||
/// 库区选择弹窗 ViewModel。
|
||||
/// 用于「拆码明细 → 库位」字段:点击库位输入框时弹出本对话框,支持按名称/编码搜索,单选确认。
|
||||
/// </summary>
|
||||
public class WarehouseAreaPickerDialogViewModel : BaseViewModel, IDialogResultable<bool>
|
||||
{
|
||||
private readonly IWarehouseAreaService _warehouseAreaService;
|
||||
|
||||
private string? _searchAreaName;
|
||||
public string? SearchAreaName
|
||||
{
|
||||
get => _searchAreaName;
|
||||
set => SetProperty(ref _searchAreaName, value);
|
||||
}
|
||||
|
||||
private string? _searchAreaCode;
|
||||
public string? SearchAreaCode
|
||||
{
|
||||
get => _searchAreaCode;
|
||||
set => SetProperty(ref _searchAreaCode, value);
|
||||
}
|
||||
|
||||
public ObservableCollection<MesXslWarehouseArea> Records { get; } = new();
|
||||
|
||||
private MesXslWarehouseArea? _selectedRecord;
|
||||
public MesXslWarehouseArea? SelectedRecord
|
||||
{
|
||||
get => _selectedRecord;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _selectedRecord, value);
|
||||
ConfirmCommand.RaiseCanExecuteChanged();
|
||||
RaisePropertyChanged(nameof(SelectedRecordDisplay));
|
||||
RaisePropertyChanged(nameof(HasSelectedRecord));
|
||||
}
|
||||
}
|
||||
|
||||
public string SelectedRecordDisplay => _selectedRecord != null
|
||||
? $"[{_selectedRecord.AreaCode}] {_selectedRecord.AreaName} · 仓库:{_selectedRecord.WarehouseName ?? "-"}"
|
||||
: "选中库区后点击「确认选择」";
|
||||
|
||||
public bool HasSelectedRecord => _selectedRecord != null;
|
||||
|
||||
private bool _result;
|
||||
public bool Result
|
||||
{
|
||||
get => _result;
|
||||
set => SetProperty(ref _result, value);
|
||||
}
|
||||
|
||||
public Action? CloseAction { get; set; }
|
||||
|
||||
public DelegateCommand SearchCommand { get; }
|
||||
public DelegateCommand ConfirmCommand { get; }
|
||||
public DelegateCommand CancelCommand { get; }
|
||||
|
||||
public WarehouseAreaPickerDialogViewModel(
|
||||
IWarehouseAreaService warehouseAreaService,
|
||||
IContainerExtension container,
|
||||
IRegionManager regionManager) : base(container, regionManager)
|
||||
{
|
||||
_warehouseAreaService = warehouseAreaService;
|
||||
SearchCommand = new DelegateCommand(async () => await LoadAsync());
|
||||
ConfirmCommand = new DelegateCommand(Confirm, () => SelectedRecord != null);
|
||||
CancelCommand = new DelegateCommand(() => CloseAction?.Invoke());
|
||||
_ = LoadAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可由外部传入「当前已选库区名称」用于打开后高亮预选。
|
||||
/// </summary>
|
||||
public void Initialize(string? currentAreaName)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(currentAreaName))
|
||||
{
|
||||
SearchAreaName = currentAreaName.Trim();
|
||||
_ = LoadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
var result = await _warehouseAreaService.PageAsync(
|
||||
1,
|
||||
500,
|
||||
areaCode: SearchAreaCode?.Trim(),
|
||||
areaName: SearchAreaName?.Trim(),
|
||||
warehouseId: null,
|
||||
status: "0");
|
||||
|
||||
Records.Clear();
|
||||
foreach (var record in result.Records)
|
||||
{
|
||||
Records.Add(record);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(SearchAreaName))
|
||||
{
|
||||
SelectedRecord = Records.FirstOrDefault(x =>
|
||||
string.Equals(x.AreaName, SearchAreaName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Records.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Confirm()
|
||||
{
|
||||
if (SelectedRecord == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Result = true;
|
||||
CloseAction?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using HandyControl.Controls;
|
||||
using HandyControl.Tools.Extension;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Services;
|
||||
|
||||
namespace YY.Admin.ViewModels.WarehouseArea;
|
||||
|
||||
public class WarehouseAreaEditDialogViewModel : BaseViewModel, IDialogResultable<bool>
|
||||
{
|
||||
private readonly IWarehouseAreaService _warehouseAreaService;
|
||||
|
||||
private MesXslWarehouseArea? _area;
|
||||
public MesXslWarehouseArea? Area
|
||||
{
|
||||
get => _area;
|
||||
set => SetProperty(ref _area, value);
|
||||
}
|
||||
|
||||
public bool IsAddMode => string.IsNullOrWhiteSpace(Area?.Id);
|
||||
public string DialogTitle => IsAddMode ? "新增库区" : "编辑库区";
|
||||
|
||||
private bool _result;
|
||||
public bool Result { get => _result; set => SetProperty(ref _result, value); }
|
||||
public Action? CloseAction { get; set; }
|
||||
|
||||
public DelegateCommand SaveCommand { get; }
|
||||
public DelegateCommand CancelCommand { get; }
|
||||
|
||||
public WarehouseAreaEditDialogViewModel(
|
||||
IWarehouseAreaService warehouseAreaService,
|
||||
IContainerExtension container,
|
||||
IRegionManager regionManager) : base(container, regionManager)
|
||||
{
|
||||
_warehouseAreaService = warehouseAreaService;
|
||||
SaveCommand = new DelegateCommand(async () => await SaveAsync());
|
||||
CancelCommand = new DelegateCommand(() => CloseAction?.Invoke());
|
||||
}
|
||||
|
||||
public void InitializeForAdd()
|
||||
{
|
||||
Area = new MesXslWarehouseArea { Status = "0" };
|
||||
RaisePropertyChanged(nameof(IsAddMode));
|
||||
RaisePropertyChanged(nameof(DialogTitle));
|
||||
}
|
||||
|
||||
public void InitializeForEdit(MesXslWarehouseArea area)
|
||||
{
|
||||
Area = new MesXslWarehouseArea
|
||||
{
|
||||
Id = area.Id,
|
||||
AreaCode = area.AreaCode,
|
||||
AreaName = area.AreaName,
|
||||
WarehouseId = area.WarehouseId,
|
||||
WarehouseName = area.WarehouseName,
|
||||
WarehouseCategory = area.WarehouseCategory,
|
||||
WarehouseCategoryName = area.WarehouseCategoryName,
|
||||
MaxCapacity = area.MaxCapacity,
|
||||
ActualCapacity = area.ActualCapacity,
|
||||
Remark = area.Remark,
|
||||
Status = area.Status,
|
||||
TenantId = area.TenantId,
|
||||
};
|
||||
RaisePropertyChanged(nameof(IsAddMode));
|
||||
RaisePropertyChanged(nameof(DialogTitle));
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
if (Area == null) return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Area.AreaCode))
|
||||
{
|
||||
MessageBox.Warning("库区编码不能为空!");
|
||||
return;
|
||||
}
|
||||
|
||||
var codeAvailable = await _warehouseAreaService.CheckAreaCodeAsync(Area.AreaCode.Trim(), Area.Id);
|
||||
if (!codeAvailable)
|
||||
{
|
||||
MessageBox.Warning("该库区编码已存在,请更换!");
|
||||
return;
|
||||
}
|
||||
|
||||
Area.AreaCode = Area.AreaCode.Trim();
|
||||
if (string.IsNullOrWhiteSpace(Area.AreaName))
|
||||
Area.AreaName = Area.AreaCode;
|
||||
|
||||
try
|
||||
{
|
||||
bool ok;
|
||||
if (IsAddMode)
|
||||
{
|
||||
ok = await _warehouseAreaService.AddAsync(Area);
|
||||
if (ok) Growl.Success("新增库区成功!");
|
||||
else { Growl.Error("新增失败,请重试!"); return; }
|
||||
}
|
||||
else
|
||||
{
|
||||
ok = await _warehouseAreaService.EditAsync(Area);
|
||||
if (ok) Growl.Success("编辑库区成功!");
|
||||
else { Growl.Error("编辑失败,请重试!"); return; }
|
||||
}
|
||||
|
||||
Result = true;
|
||||
CloseAction?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Growl.Error($"保存失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
using HandyControl.Controls;
|
||||
using HandyControl.Tools.Extension;
|
||||
using Prism.Events;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Events;
|
||||
using YY.Admin.Core.Helper;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.Views.WarehouseArea;
|
||||
|
||||
namespace YY.Admin.ViewModels.WarehouseArea;
|
||||
|
||||
public class WarehouseAreaListViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IWarehouseAreaService _warehouseAreaService;
|
||||
private readonly IWarehouseService _warehouseService;
|
||||
private readonly IDialogService _dialogService;
|
||||
private SubscriptionToken? _changedToken;
|
||||
private SubscriptionToken? _syncConflictToken;
|
||||
|
||||
private ObservableCollection<MesXslWarehouseArea> _areas = new();
|
||||
public ObservableCollection<MesXslWarehouseArea> Areas
|
||||
{
|
||||
get => _areas;
|
||||
set => SetProperty(ref _areas, value);
|
||||
}
|
||||
|
||||
private long _total;
|
||||
public long Total { get => _total; set => SetProperty(ref _total, value); }
|
||||
|
||||
private int _pageNo = 1;
|
||||
public int PageNo { get => _pageNo; set => SetProperty(ref _pageNo, value); }
|
||||
|
||||
private int _pageSize = 20;
|
||||
public int PageSize { get => _pageSize; set => SetProperty(ref _pageSize, value); }
|
||||
|
||||
private string? _filterAreaCode;
|
||||
public string? FilterAreaCode { get => _filterAreaCode; set => SetProperty(ref _filterAreaCode, value); }
|
||||
|
||||
private string? _filterAreaName;
|
||||
public string? FilterAreaName { get => _filterAreaName; set => SetProperty(ref _filterAreaName, value); }
|
||||
|
||||
private string? _filterWarehouseId;
|
||||
public string? FilterWarehouseId { get => _filterWarehouseId; set => SetProperty(ref _filterWarehouseId, value); }
|
||||
|
||||
private string? _filterStatus;
|
||||
public string? FilterStatus { get => _filterStatus; set => SetProperty(ref _filterStatus, value); }
|
||||
|
||||
public ObservableCollection<KeyValuePair<string, string>> WarehouseOptions { get; } = new();
|
||||
public ObservableCollection<KeyValuePair<string, string>> StatusOptions { get; } = new();
|
||||
|
||||
public DelegateCommand SearchCommand { get; }
|
||||
public DelegateCommand ResetCommand { get; }
|
||||
public DelegateCommand AddCommand { get; }
|
||||
public DelegateCommand<MesXslWarehouseArea> EditCommand { get; }
|
||||
public DelegateCommand<MesXslWarehouseArea> DeleteCommand { get; }
|
||||
public DelegateCommand<MesXslWarehouseArea> ToggleStatusCommand { get; }
|
||||
public DelegateCommand PrevPageCommand { get; }
|
||||
public DelegateCommand NextPageCommand { get; }
|
||||
|
||||
public WarehouseAreaListViewModel(
|
||||
IWarehouseAreaService warehouseAreaService,
|
||||
IWarehouseService warehouseService,
|
||||
IContainerExtension container,
|
||||
IDialogService dialogService,
|
||||
IRegionManager regionManager) : base(container, regionManager)
|
||||
{
|
||||
_warehouseAreaService = warehouseAreaService;
|
||||
_warehouseService = warehouseService;
|
||||
_dialogService = dialogService;
|
||||
|
||||
SearchCommand = new DelegateCommand(async () => { PageNo = 1; await LoadAsync(); });
|
||||
ResetCommand = new DelegateCommand(async () =>
|
||||
{
|
||||
FilterAreaCode = null;
|
||||
FilterAreaName = null;
|
||||
FilterWarehouseId = null;
|
||||
FilterStatus = null;
|
||||
PageNo = 1;
|
||||
await LoadAsync();
|
||||
});
|
||||
AddCommand = new DelegateCommand(async () => await ShowAddDialogAsync());
|
||||
EditCommand = new DelegateCommand<MesXslWarehouseArea>(async a => await ShowEditDialogAsync(a));
|
||||
DeleteCommand = new DelegateCommand<MesXslWarehouseArea>(async a => await DeleteAsync(a));
|
||||
ToggleStatusCommand = new DelegateCommand<MesXslWarehouseArea>(async a => await ToggleStatusAsync(a));
|
||||
PrevPageCommand = new DelegateCommand(async () => { if (PageNo > 1) { PageNo--; await LoadAsync(); } });
|
||||
NextPageCommand = new DelegateCommand(async () => { if ((long)PageNo * PageSize < Total) { PageNo++; await LoadAsync(); } });
|
||||
|
||||
_changedToken = _eventAggregator
|
||||
.GetEvent<WarehouseAreaChangedEvent>()
|
||||
.Subscribe(async p => await OnChangedAsync(p), ThreadOption.UIThread);
|
||||
|
||||
_syncConflictToken = _eventAggregator.GetEvent<SyncConflictEvent>()
|
||||
.Subscribe(OnSyncConflict, ThreadOption.UIThread);
|
||||
|
||||
_ = InitializeAsync();
|
||||
}
|
||||
|
||||
private async Task OnChangedAsync(WarehouseAreaChangedPayload payload)
|
||||
{
|
||||
if (payload.Action == "edit" && !string.IsNullOrWhiteSpace(payload.WarehouseAreaId))
|
||||
await RefreshSingleAsync(payload.WarehouseAreaId!);
|
||||
else
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
private async Task RefreshSingleAsync(string id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var updated = await _warehouseAreaService.GetByIdAsync(id);
|
||||
if (updated == null) return;
|
||||
var idx = Areas.ToList().FindIndex(a => string.Equals(a.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||
if (idx >= 0) Areas[idx] = updated;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[库区] 单条刷新失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSyncConflict(SyncConflictPayload payload)
|
||||
{
|
||||
if (!string.Equals(payload.EntityName, "库区", StringComparison.OrdinalIgnoreCase)) return;
|
||||
|
||||
var parts = new List<string>();
|
||||
if (payload.PushedCount > 0) parts.Add($"已同步 {payload.PushedCount} 条本地改动到服务器");
|
||||
if (payload.NewRecordsPushed > 0) parts.Add($"已上传 {payload.NewRecordsPushed} 条本地新增记录");
|
||||
if (payload.ConflictCount > 0) parts.Add($"{payload.ConflictCount} 条记录与服务器版本冲突,已保留服务器版本");
|
||||
|
||||
if (parts.Count == 0) return;
|
||||
var message = string.Join("\n", parts);
|
||||
if (payload.ConflictCount > 0) Growl.Warning(message);
|
||||
else Growl.Success(message);
|
||||
}
|
||||
|
||||
private async Task InitializeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusOptions.Clear();
|
||||
StatusOptions.Add(new KeyValuePair<string, string>("全部", ""));
|
||||
StatusOptions.Add(new KeyValuePair<string, string>("启用", "0"));
|
||||
StatusOptions.Add(new KeyValuePair<string, string>("停用", "1"));
|
||||
|
||||
await LoadWarehouseOptionsAsync();
|
||||
await UIHelper.WaitForRenderAsync();
|
||||
await LoadAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"库区列表初始化失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadWarehouseOptionsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var warehouses = await _warehouseService.GetAllAsync();
|
||||
WarehouseOptions.Clear();
|
||||
WarehouseOptions.Add(new KeyValuePair<string, string>("全部", ""));
|
||||
foreach (var w in warehouses.Where(w => !string.IsNullOrWhiteSpace(w.Id)))
|
||||
WarehouseOptions.Add(new KeyValuePair<string, string>(w.WarehouseName ?? w.Id!, w.Id!));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[库区] 加载仓库选项失败: {ex.Message}");
|
||||
if (WarehouseOptions.Count == 0)
|
||||
WarehouseOptions.Add(new KeyValuePair<string, string>("全部", ""));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoadAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
var result = await _warehouseAreaService.PageAsync(PageNo, PageSize, FilterAreaCode, FilterAreaName, FilterWarehouseId, FilterStatus);
|
||||
Areas = new ObservableCollection<MesXslWarehouseArea>(result.Records);
|
||||
Total = result.Total;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Growl.Error($"加载库区列表失败:{ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowAddDialogAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await HandyControl.Controls.Dialog.Show<WarehouseAreaEditDialogView>()
|
||||
.Initialize<WarehouseAreaEditDialogViewModel>(vm => vm.InitializeForAdd())
|
||||
.GetResultAsync<bool>();
|
||||
if (result) await LoadAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Growl.Error($"打开新增对话框失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowEditDialogAsync(MesXslWarehouseArea area)
|
||||
{
|
||||
if (area == null) return;
|
||||
try
|
||||
{
|
||||
var result = await HandyControl.Controls.Dialog.Show<WarehouseAreaEditDialogView>()
|
||||
.Initialize<WarehouseAreaEditDialogViewModel>(vm => vm.InitializeForEdit(area))
|
||||
.GetResultAsync<bool>();
|
||||
if (result) await LoadAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Growl.Error($"打开编辑对话框失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteAsync(MesXslWarehouseArea area)
|
||||
{
|
||||
if (area?.Id == null) return;
|
||||
var confirm = System.Windows.MessageBox.Show($"确定删除库区「{area.AreaCode}」?此操作不可恢复!", "确认删除",
|
||||
MessageBoxButton.OKCancel, MessageBoxImage.Question);
|
||||
if (confirm != System.Windows.MessageBoxResult.OK) return;
|
||||
|
||||
var ok = await _warehouseAreaService.DeleteAsync(area.Id);
|
||||
if (ok) { Growl.Success("删除成功!"); await LoadAsync(); }
|
||||
else Growl.Error("删除失败!");
|
||||
}
|
||||
|
||||
private async Task ToggleStatusAsync(MesXslWarehouseArea area)
|
||||
{
|
||||
if (area?.Id == null) return;
|
||||
var newStatus = area.Status == "1" ? "0" : "1";
|
||||
var ok = await _warehouseAreaService.UpdateStatusAsync(area.Id, newStatus);
|
||||
if (ok) { area.Status = newStatus; RaisePropertyChanged(nameof(Areas)); }
|
||||
else Growl.Error("状态切换失败!");
|
||||
}
|
||||
|
||||
protected override void CleanUp()
|
||||
{
|
||||
base.CleanUp();
|
||||
if (_changedToken != null)
|
||||
{
|
||||
_eventAggregator.GetEvent<WarehouseAreaChangedEvent>().Unsubscribe(_changedToken);
|
||||
_changedToken = null;
|
||||
}
|
||||
if (_syncConflictToken != null)
|
||||
{
|
||||
_eventAggregator.GetEvent<SyncConflictEvent>().Unsubscribe(_syncConflictToken);
|
||||
_syncConflictToken = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,8 +119,8 @@
|
||||
</DataTemplate>
|
||||
</DataGrid.RowHeaderTemplate>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="条码" Binding="{Binding Barcode}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="150"/>
|
||||
<DataGridTextColumn Header="批次号" Binding="{Binding BatchNo}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="130"/>
|
||||
<DataGridTextColumn Header="条码" Binding="{Binding Barcode}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="200"/>
|
||||
<DataGridTextColumn Header="批次号" Binding="{Binding BatchNo}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="180"/>
|
||||
<DataGridTextColumn Header="入场日期" Binding="{Binding EntryDateText}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="100"/>
|
||||
<DataGridTextColumn Header="物料名称" Binding="{Binding MaterialName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="150"/>
|
||||
<DataGridTextColumn Header="供应商名称" Binding="{Binding SupplierName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="140"/>
|
||||
|
||||
@@ -116,8 +116,8 @@
|
||||
</DataTemplate>
|
||||
</DataGrid.RowHeaderTemplate>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="条码" Binding="{Binding Barcode}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="140"/>
|
||||
<DataGridTextColumn Header="批次号" Binding="{Binding BatchNo}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="120"/>
|
||||
<DataGridTextColumn Header="条码" Binding="{Binding Barcode}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="200"/>
|
||||
<DataGridTextColumn Header="批次号" Binding="{Binding BatchNo}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="180"/>
|
||||
<DataGridTextColumn Header="入场时间" Binding="{Binding EntryTime, StringFormat='yyyy-MM-dd HH:mm'}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="140"/>
|
||||
<DataGridTextColumn Header="榜单号" Binding="{Binding BillNo}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="130"/>
|
||||
<DataGridTextColumn Header="物料名称" Binding="{Binding MaterialName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="140"/>
|
||||
|
||||
@@ -82,14 +82,22 @@
|
||||
|
||||
<hc:ScrollViewer Grid.Column="0" IsInertiaEnabled="True" HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel x:Name="RootPanel" Margin="24,8,24,8">
|
||||
<TextBlock Text="基础资料"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||
Margin="0,0,0,6"/>
|
||||
<DockPanel Margin="0,0,0,6">
|
||||
<Button DockPanel.Dock="Right"
|
||||
Content="重置"
|
||||
Command="{Binding ResetCommand}"
|
||||
Style="{StaticResource ButtonDefault}"
|
||||
Width="80" Height="28" FontSize="12"/>
|
||||
<TextBlock Text="基础资料"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||
VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
|
||||
<!-- 带横竖线的表格式表单 -->
|
||||
<Border BorderThickness="1" BorderBrush="{DynamicResource BorderBrush}" Margin="0,0,0,8">
|
||||
<!-- 带横竖线的表格式表单;已打印时整体禁用(IsTotalWeightEditable=false),IsEnabled 向下继承到所有子控件 -->
|
||||
<Border BorderThickness="1" BorderBrush="{DynamicResource BorderBrush}" Margin="0,0,0,8"
|
||||
IsEnabled="{Binding IsTotalWeightEditable}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="36"/> <!-- 密炼物料 -->
|
||||
@@ -97,7 +105,7 @@
|
||||
<RowDefinition Height="36"/> <!-- 榜单号 -->
|
||||
<RowDefinition Height="36"/> <!-- 供料客户 / 供应商名称 -->
|
||||
<RowDefinition Height="36"/> <!-- 厂家物料名称 / 保质期 -->
|
||||
<RowDefinition Height="36"/> <!-- 总重 / 总份数 -->
|
||||
<RowDefinition Height="40"/> <!-- 总重 / 总份数 — 多 4px 给 NumericUpDown 上下间距,否则 spinner 箭头会被裁切 -->
|
||||
<RowDefinition Height="36"/> <!-- 每份总重 / 每份包数 -->
|
||||
<RowDefinition Height="36"/> <!-- 库位 / 卸货人 -->
|
||||
<RowDefinition Height="60"/> <!-- 备注 -->
|
||||
@@ -248,7 +256,7 @@
|
||||
FontSize="12" Foreground="{DynamicResource PrimaryTextBrush}"/>
|
||||
</Border>
|
||||
<Border Grid.Row="3" Grid.Column="3"
|
||||
BorderThickness="0,0,0,1" BorderBrush="{DynamicResource BorderBrush}" Padding="4,0">
|
||||
BorderThickness="0,0,0,1" BorderBrush="{DynamicResource BorderBrush}" Padding="6,0">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
@@ -277,14 +285,15 @@
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
<!-- 与「密炼物料 / 榜单号」两行的「选择 + 清除」按钮参数完全一致:右端齐平 + 内外边距统一 -->
|
||||
<Button Grid.Column="1" Content="选 择"
|
||||
Command="{Binding OpenSupplierPickerCommand}"
|
||||
Style="{StaticResource ButtonPrimary}"
|
||||
Height="26" Margin="4,0,0,0" FontSize="12" Padding="8,0"/>
|
||||
Height="26" Margin="6,0,0,0" FontSize="12" Padding="10,0"/>
|
||||
<Button Grid.Column="2"
|
||||
Command="{Binding ClearSupplierCommand}"
|
||||
Style="{StaticResource ButtonIcon}"
|
||||
Height="26" Width="24" Margin="2,0,0,0" Padding="0"
|
||||
Height="26" Width="24" Margin="4,0,0,0" Padding="0"
|
||||
ToolTip="清除供应商"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
hc:IconElement.Geometry="{StaticResource ErrorGeometry}"/>
|
||||
@@ -324,9 +333,14 @@
|
||||
FontSize="12" Foreground="{DynamicResource PrimaryTextBrush}"/>
|
||||
</Border>
|
||||
<Border Grid.Row="5" Grid.Column="1"
|
||||
BorderThickness="0,0,1,1" BorderBrush="{DynamicResource BorderBrush}" Padding="4,4">
|
||||
BorderThickness="0,0,1,1" BorderBrush="{DynamicResource BorderBrush}" Padding="4,2">
|
||||
<!--
|
||||
显式 Height + VerticalAlignment=Center 避免被 Border 拉伸到 36+px,
|
||||
否则 HandyControl 的 NumericUpDownPlus 内置 spinner 会被压扁导致箭头显示不全。
|
||||
-->
|
||||
<hc:NumericUpDown Value="{Binding TotalWeightInput}"
|
||||
Minimum="0" DecimalPlaces="2"
|
||||
Height="30" VerticalAlignment="Center"
|
||||
Style="{StaticResource NumericUpDownPlus}"
|
||||
hc:InfoElement.Placeholder="请输入总重"/>
|
||||
</Border>
|
||||
@@ -338,8 +352,26 @@
|
||||
</Border>
|
||||
<Border Grid.Row="5" Grid.Column="3"
|
||||
BorderThickness="0,0,0,1" BorderBrush="{DynamicResource BorderBrush}" Padding="6,0">
|
||||
<TextBlock Text="{Binding SplitTotalPortionsDisplay}" VerticalAlignment="Center"
|
||||
FontSize="13" Foreground="{DynamicResource SecondaryTextBrush}"/>
|
||||
<!-- 由拆码明细聚合而来:未维护明细时显示灰色占位提示,提示用户操作路径 -->
|
||||
<Grid>
|
||||
<TextBlock Text="{Binding SplitTotalPortionsDisplay}" VerticalAlignment="Center"
|
||||
FontSize="13" Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Text="由拆码明细自动生成" VerticalAlignment="Center"
|
||||
FontSize="12" Foreground="#BFBFBF" FontStyle="Italic"
|
||||
IsHitTestVisible="False">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding SplitTotalPortionsDisplay}" Value="">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ===== Row 6: 每份总重(KG) / 每份包数 ===== -->
|
||||
@@ -352,8 +384,25 @@
|
||||
</Border>
|
||||
<Border Grid.Row="6" Grid.Column="1"
|
||||
BorderThickness="0,0,1,1" BorderBrush="{DynamicResource BorderBrush}" Padding="6,0">
|
||||
<TextBlock Text="{Binding SplitPortionWeightDisplay}" VerticalAlignment="Center"
|
||||
FontSize="13" Foreground="{DynamicResource SecondaryTextBrush}"/>
|
||||
<Grid>
|
||||
<TextBlock Text="{Binding SplitPortionWeightDisplay}" VerticalAlignment="Center"
|
||||
FontSize="13" Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Text="由拆码明细自动生成" VerticalAlignment="Center"
|
||||
FontSize="12" Foreground="#BFBFBF" FontStyle="Italic"
|
||||
IsHitTestVisible="False">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding SplitPortionWeightDisplay}" Value="">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border Grid.Row="6" Grid.Column="2"
|
||||
Background="{DynamicResource SecondaryRegionBrush}"
|
||||
@@ -363,8 +412,25 @@
|
||||
</Border>
|
||||
<Border Grid.Row="6" Grid.Column="3"
|
||||
BorderThickness="0,0,0,1" BorderBrush="{DynamicResource BorderBrush}" Padding="6,0">
|
||||
<TextBlock Text="{Binding SplitPortionPackagesDisplay}" VerticalAlignment="Center"
|
||||
FontSize="13" Foreground="{DynamicResource SecondaryTextBrush}"/>
|
||||
<Grid>
|
||||
<TextBlock Text="{Binding SplitPortionPackagesDisplay}" VerticalAlignment="Center"
|
||||
FontSize="13" Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Text="由拆码明细自动生成" VerticalAlignment="Center"
|
||||
FontSize="12" Foreground="#BFBFBF" FontStyle="Italic"
|
||||
IsHitTestVisible="False">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding SplitPortionPackagesDisplay}" Value="">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ===== Row 7: 库位 / 卸货人 ===== -->
|
||||
@@ -528,13 +594,29 @@
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button DockPanel.Dock="Right"
|
||||
Content="新增明细"
|
||||
Command="{Binding AddSplitDetailCommand}"
|
||||
Style="{StaticResource ButtonPrimary}"
|
||||
Height="30"
|
||||
Padding="10,0"
|
||||
FontSize="12"/>
|
||||
<!-- 右侧按钮组:新增明细(始终可用,支持「继续拆码」) + 重新拆码(仅编辑态可用) -->
|
||||
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<!--
|
||||
「新增明细」:始终可用。
|
||||
已打印记录上点击 → 追加一行 HasCard=false 的可编辑行;
|
||||
再点「生成原材料卡片」时按 HasCard 过滤,只对新增的行加卡(条码从已有数 +1 续编)。
|
||||
-->
|
||||
<Button Content="新增明细"
|
||||
Command="{Binding AddSplitDetailCommand}"
|
||||
Style="{StaticResource ButtonPrimary}"
|
||||
Height="30"
|
||||
Padding="10,0"
|
||||
FontSize="12"
|
||||
ToolTip="新增一行待拆码明细。已打印记录追加新行时支持「继续拆码」"/>
|
||||
<Button Content="重新拆码"
|
||||
Command="{Binding ResplitCommand}"
|
||||
Style="{StaticResource ButtonWarning}"
|
||||
Height="30"
|
||||
Padding="10,0"
|
||||
Margin="8,0,0,0"
|
||||
FontSize="12"
|
||||
ToolTip="清除该入场记录已生成的原材料卡片并清空明细,重新进行拆码"/>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
|
||||
<StackPanel>
|
||||
@@ -546,6 +628,7 @@
|
||||
<ColumnDefinition Width="100"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="90"/>
|
||||
<ColumnDefinition Width="90"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="份数"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
@@ -559,11 +642,17 @@
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
FontWeight="SemiBold" FontSize="13"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"/>
|
||||
<!-- 库位列:保存时非必填;点击「生成原材料卡片」时必填,ToolTip 给出说明 -->
|
||||
<TextBlock Grid.Column="3" Text="库位"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
FontWeight="SemiBold" FontSize="13"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||
ToolTip="生成原材料卡片时为必填项"/>
|
||||
<TextBlock Grid.Column="4" Text="打印标记"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
FontWeight="SemiBold" FontSize="13"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"/>
|
||||
<TextBlock Grid.Column="4" Text="操作"
|
||||
<TextBlock Grid.Column="5" Text="操作"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
FontWeight="SemiBold" FontSize="13"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"/>
|
||||
@@ -591,6 +680,35 @@
|
||||
<ItemsControl ItemsSource="{Binding SplitCodeDetails}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<DataTemplate.Resources>
|
||||
<!--
|
||||
已生成卡片的「行」锁定样式(HasCard==true):
|
||||
把份数 / 每份重量 / 每份包数 三个 TextBox 设为只读 + 淡灰背景。
|
||||
配合"该行隐藏删除按钮 + 库位按钮禁用"形成完整防护,避免改了行内数值后
|
||||
与已生成的原材料卡片数据脱节。
|
||||
业务流程:用户在已打印记录上想继续拆码 → 新增行(HasCard=false 可编辑)
|
||||
→ 点「生成原材料卡片」只对未生成行加卡(条码续号续接)。
|
||||
-->
|
||||
<Style x:Key="LockableSplitTextBoxStyle" TargetType="TextBox">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="Height" Value="32"/>
|
||||
<Setter Property="BorderBrush" Value="#D9D9D9"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Background" Value="White"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="Margin" Value="4,0"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasCard}" Value="True">
|
||||
<Setter Property="IsReadOnly" Value="True"/>
|
||||
<Setter Property="Background" Value="#F5F5F5"/>
|
||||
<Setter Property="Foreground" Value="#8C8C8C"/>
|
||||
<Setter Property="ToolTip" Value="该行已生成原材料卡片,不可修改。如需调整请先点「重新拆码」清空全部卡片"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</DataTemplate.Resources>
|
||||
<Grid Height="44">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="80"/>
|
||||
@@ -598,42 +716,156 @@
|
||||
<ColumnDefinition Width="100"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="90"/>
|
||||
<ColumnDefinition Width="90"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border Grid.Column="0" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
|
||||
<TextBox Text="{Binding Portions, UpdateSourceTrigger=PropertyChanged}"
|
||||
HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
|
||||
VerticalAlignment="Center" Height="32"
|
||||
BorderBrush="#D9D9D9" BorderThickness="1"
|
||||
Background="White" FontSize="13" Margin="4,0"/>
|
||||
Style="{StaticResource LockableSplitTextBoxStyle}"/>
|
||||
</Border>
|
||||
<Border Grid.Column="1" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
|
||||
<TextBox Text="{Binding PortionWeight, UpdateSourceTrigger=PropertyChanged}"
|
||||
HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
|
||||
VerticalAlignment="Center" Height="32"
|
||||
BorderBrush="#D9D9D9" BorderThickness="1"
|
||||
Background="White" FontSize="13" Margin="4,0"/>
|
||||
Style="{StaticResource LockableSplitTextBoxStyle}"/>
|
||||
</Border>
|
||||
<Border Grid.Column="2" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
|
||||
<TextBox Text="{Binding PortionPackages, UpdateSourceTrigger=PropertyChanged}"
|
||||
HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
|
||||
VerticalAlignment="Center" Height="32"
|
||||
BorderBrush="#D9D9D9" BorderThickness="1"
|
||||
Background="White" FontSize="13" Margin="4,0"/>
|
||||
Style="{StaticResource LockableSplitTextBoxStyle}"/>
|
||||
</Border>
|
||||
<Border Grid.Column="3" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
|
||||
<TextBox Text="{Binding WarehouseLocation, UpdateSourceTrigger=PropertyChanged}"
|
||||
HorizontalContentAlignment="Stretch" VerticalContentAlignment="Center"
|
||||
VerticalAlignment="Center" Height="32"
|
||||
BorderBrush="#D9D9D9" BorderThickness="1"
|
||||
Background="White" FontSize="13" Padding="8,0" Margin="4,0"/>
|
||||
<!--
|
||||
InputBindings 内 RelativeSource 不在可视树中,查找会静默失败。
|
||||
改用 Button + ControlTemplate:Command 写在 Button 元素上,可视树正常,RelativeSource 可靠。
|
||||
-->
|
||||
<Button Command="{Binding DataContext.OpenWarehouseAreaPickerCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
|
||||
CommandParameter="{Binding}"
|
||||
Cursor="Hand"
|
||||
VerticalAlignment="Center"
|
||||
Height="32"
|
||||
Margin="4,0"
|
||||
Focusable="False">
|
||||
<!-- 行级锁定:HasCard==true(该行已生成卡片)时禁用,与三个数字字段一致 -->
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="IsEnabled" Value="True"/>
|
||||
<Setter Property="ToolTip" Value="点击选择库区"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasCard}" Value="True">
|
||||
<Setter Property="IsEnabled" Value="False"/>
|
||||
<Setter Property="ToolTip" Value="该行已生成原材料卡片,不可修改。如需调整请先点「重新拆码」清空全部卡片"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
<Button.Template>
|
||||
<!-- 视觉与同行的 TextBox 对齐:相同高度/边框色/圆角/字号,内容水平居中 -->
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd"
|
||||
BorderBrush="#D9D9D9" BorderThickness="1"
|
||||
CornerRadius="2"
|
||||
Background="White">
|
||||
<Grid>
|
||||
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
FontSize="13" TextTrimming="CharacterEllipsis"
|
||||
Text="{Binding WarehouseLocation}">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="#1F1F1F"/>
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding WarehouseLocation}" Value="">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding WarehouseLocation}" Value="{x:Null}">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
FontSize="13" Text="点击选择库区" Foreground="#BFBFBF">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding WarehouseLocation}" Value="">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding WarehouseLocation}" Value="{x:Null}">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="#4096FF"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#F0F7FF"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
</Border>
|
||||
<!--
|
||||
打印标记列:行级状态(HasCard),与「继续拆码」流程契合。
|
||||
旧行(已生成卡片)显示绿色「已打印」;新增的待生成行显示灰色「未打印」。
|
||||
-->
|
||||
<Border Grid.Column="4" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
|
||||
<Grid>
|
||||
<Border CornerRadius="2" Padding="6,2" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#F5F5F5"/>
|
||||
<Setter Property="BorderBrush" Value="#D9D9D9"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasCard}" Value="True">
|
||||
<Setter Property="Background" Value="#F6FFED"/>
|
||||
<Setter Property="BorderBrush" Value="#B7EB8F"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock VerticalAlignment="Center" FontSize="12">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Text" Value="未打印"/>
|
||||
<Setter Property="Foreground" Value="#8C8C8C"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasCard}" Value="True">
|
||||
<Setter Property="Text" Value="已打印"/>
|
||||
<Setter Property="Foreground" Value="#52C41A"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border Grid.Column="5" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
|
||||
<!-- 行级:HasCard==true(该行已生成卡片)时隐藏删除按钮,新增的待生成行可正常删除 -->
|
||||
<Button Content="删除"
|
||||
Command="{Binding DataContext.RemoveSplitDetailCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
|
||||
CommandParameter="{Binding}"
|
||||
Style="{StaticResource ButtonDanger}"
|
||||
VerticalAlignment="Center" Height="28"
|
||||
Padding="6,0" Margin="12,0" FontSize="11"/>
|
||||
Padding="6,0" Margin="12,0" FontSize="11">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button" BasedOn="{StaticResource ButtonDanger}">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasCard}" Value="True">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
</Border>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
@@ -662,70 +894,115 @@
|
||||
BorderThickness="1,0,0,0"
|
||||
Background="{DynamicResource RegionBrush}">
|
||||
<DockPanel LastChildFill="True">
|
||||
<Border DockPanel.Dock="Top" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1" Padding="8,6">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock Text="今日入场" FontWeight="SemiBold" FontSize="13"
|
||||
|
||||
<!-- ===== 标题 + 日期筛选 ===== -->
|
||||
<Border DockPanel.Dock="Top" BorderBrush="{DynamicResource BorderBrush}"
|
||||
BorderThickness="0,0,0,1" Padding="8,6">
|
||||
<StackPanel>
|
||||
<DockPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="原料入场记录" FontWeight="SemiBold" FontSize="13"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"/>
|
||||
<TextBlock Text="按入场/创建日期为本日的记录" FontSize="11" Opacity="0.65"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}" TextWrapping="Wrap"/>
|
||||
<TextBlock Text="点击记录可回填到左侧表单" FontSize="11" Opacity="0.65"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Content="刷新" Margin="4,0"
|
||||
<Button DockPanel.Dock="Right" Content="刷新" Margin="4,0,0,0"
|
||||
Command="{Binding RefreshTodayEntriesCommand}"
|
||||
Style="{StaticResource ButtonPrimary}" Padding="8,2" FontSize="11" Height="26"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
Style="{StaticResource ButtonPrimary}"
|
||||
Padding="8,2" FontSize="11" Height="26"
|
||||
VerticalAlignment="Top"/>
|
||||
</DockPanel>
|
||||
<ComboBox ItemsSource="{Binding DateRangeOptions}"
|
||||
SelectedItem="{Binding SelectedDateRange}"
|
||||
Margin="0,6,0,0" Height="26" FontSize="12"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border DockPanel.Dock="Top" Background="{DynamicResource SecondaryRegionBrush}" Height="28" Padding="8,0">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="条码" FontWeight="SemiBold" FontSize="11"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"/>
|
||||
<TextBlock Grid.Column="1" Text="物料" FontWeight="SemiBold" FontSize="11"
|
||||
Margin="6,0,0,0" VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
<!-- ===== 入场记录列表(卡片式,支持换行) ===== -->
|
||||
<ListBox ItemsSource="{Binding TodayEntries}"
|
||||
SelectedItem="{Binding SelectedTodayEntry, Mode=TwoWay}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
BorderThickness="0"
|
||||
Background="Transparent"
|
||||
ItemContainerStyle="{StaticResource TodayEntryListBoxItemStyle}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type core:MesXslRawMaterialEntry}">
|
||||
<Border BorderBrush="{DynamicResource BorderBrush}"
|
||||
BorderThickness="0,0,0,1" Padding="8,8">
|
||||
<StackPanel>
|
||||
|
||||
<ListBox ItemsSource="{Binding TodayEntries}"
|
||||
SelectedItem="{Binding SelectedTodayEntry, Mode=TwoWay}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
BorderThickness="0"
|
||||
Background="Transparent"
|
||||
ItemContainerStyle="{StaticResource TodayEntryListBoxItemStyle}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type core:MesXslRawMaterialEntry}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="{Binding Barcode}" FontSize="12"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
<!-- 第一行:条码 + 打印状态 -->
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Right"
|
||||
Text="{Binding PrintFlagText}"
|
||||
FontSize="13" Margin="6,0,0,0"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
VerticalAlignment="Top"/>
|
||||
<TextBlock FontSize="14" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||
ToolTip="{Binding Barcode}">
|
||||
<Run Text="条码:" FontWeight="SemiBold"/>
|
||||
<Run Text="{Binding Barcode}"/>
|
||||
</TextBlock>
|
||||
</DockPanel>
|
||||
|
||||
<!-- 第二行:榜单号 -->
|
||||
<TextBlock FontSize="14" TextWrapping="Wrap"
|
||||
Margin="0,5,0,0"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||
ToolTip="{Binding Barcode}"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding MaterialName}" FontSize="12"
|
||||
TextTrimming="CharacterEllipsis" Margin="6,0,0,0"
|
||||
ToolTip="{Binding BillNo}">
|
||||
<Run Text="榜单号:" FontWeight="SemiBold"/>
|
||||
<Run Text="{Binding BillNo}"/>
|
||||
</TextBlock>
|
||||
|
||||
<!-- 第三行:批次号(加黑加粗) -->
|
||||
<TextBlock FontSize="14" TextWrapping="Wrap"
|
||||
FontWeight="Bold"
|
||||
Margin="0,5,0,0"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||
ToolTip="{Binding BatchNo}">
|
||||
<Run Text="批次号:"/>
|
||||
<Run Text="{Binding BatchNo}"/>
|
||||
</TextBlock>
|
||||
|
||||
<!-- 物料 + 总重 -->
|
||||
<DockPanel Margin="0,5,0,0">
|
||||
<TextBlock DockPanel.Dock="Right"
|
||||
FontSize="13" Margin="6,0,0,0"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
ToolTip="{Binding TotalWeight, StringFormat={}{0:0.##} KG}"
|
||||
VerticalAlignment="Top">
|
||||
<Run Text="总重:" FontWeight="SemiBold"/>
|
||||
<Run Text="{Binding TotalWeight, StringFormat={}{0:0.##}KG}"/>
|
||||
</TextBlock>
|
||||
<TextBlock FontSize="14" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
ToolTip="{Binding MaterialName}">
|
||||
<Run Text="物料:" FontWeight="SemiBold"/>
|
||||
<Run Text="{Binding MaterialName}"/>
|
||||
</TextBlock>
|
||||
</DockPanel>
|
||||
|
||||
<!-- 供应商 -->
|
||||
<TextBlock FontSize="13" TextWrapping="Wrap"
|
||||
Margin="0,4,0,0" Opacity="0.7"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
ToolTip="{Binding MaterialName}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
ToolTip="{Binding SupplierName}">
|
||||
<Run Text="供应商:" FontWeight="SemiBold"/>
|
||||
<Run Text="{Binding SupplierName}"/>
|
||||
</TextBlock>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,12,0,20">
|
||||
<Button Content="重置" Command="{Binding ResetCommand}" Style="{StaticResource ButtonDefault}" Margin="0,0,15,0" Width="100"/>
|
||||
<Button Content="保存" Command="{Binding SaveCommand}" Style="{StaticResource ButtonPrimary}" Width="100" Margin="0,0,15,0"/>
|
||||
<Button Content="生成原材料卡片"
|
||||
Command="{Binding GenerateRawMaterialCardsCommand}"
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
<UserControl x:Class="YY.Admin.Views.RawMaterialEntry.WarehouseAreaPickerDialogView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||
mc:Ignorable="d"
|
||||
Width="760">
|
||||
|
||||
<Grid Background="{DynamicResource ThirdlyRegionBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="360"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题 -->
|
||||
<hc:SimplePanel Margin="20,16,20,12">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Border Width="32" Height="32" CornerRadius="6" Background="{DynamicResource PrimaryBrush}" Margin="0,0,10,0">
|
||||
<!-- MaterialDesignThemes 的 PackIconKind 没有 WarehouseOutline,仅 Warehouse 可用,否则 TypeConverter 解析时抛异常 -->
|
||||
<md:PackIcon Kind="Warehouse" Width="18" Height="18" Foreground="White"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<TextBlock Text="选择库区" FontSize="16" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<Button Width="22" Height="22" Command="hc:ControlCommands.Close"
|
||||
Style="{StaticResource ButtonIcon}"
|
||||
Foreground="{DynamicResource PrimaryBrush}"
|
||||
hc:IconElement.Geometry="{StaticResource ErrorGeometry}"
|
||||
Padding="0" HorizontalAlignment="Right" VerticalAlignment="Center"/>
|
||||
</hc:SimplePanel>
|
||||
|
||||
<!-- 搜索 -->
|
||||
<Border Grid.Row="1" Background="{DynamicResource RegionBrush}" Padding="16,10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="80"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<hc:TextBox Text="{Binding SearchAreaName, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Placeholder="输入库区名称搜索..."
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,8,0">
|
||||
<hc:TextBox.InputBindings>
|
||||
<KeyBinding Key="Enter" Command="{Binding SearchCommand}"/>
|
||||
</hc:TextBox.InputBindings>
|
||||
</hc:TextBox>
|
||||
<hc:TextBox Grid.Column="1"
|
||||
Text="{Binding SearchAreaCode, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Placeholder="输入库区编码搜索..."
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,8,0">
|
||||
<hc:TextBox.InputBindings>
|
||||
<KeyBinding Key="Enter" Command="{Binding SearchCommand}"/>
|
||||
</hc:TextBox.InputBindings>
|
||||
</hc:TextBox>
|
||||
<Button Grid.Column="2" Command="{Binding SearchCommand}"
|
||||
IsEnabled="{Binding IsLoading, Converter={StaticResource Boolean2BooleanReConverter}}"
|
||||
Style="{StaticResource ButtonPrimary}" Height="32">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="Magnify" Width="14" Height="14" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||
<TextBlock Text="搜索" FontSize="13" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 列表 -->
|
||||
<DataGrid x:Name="WarehouseAreasGrid"
|
||||
Grid.Row="2"
|
||||
Margin="16,8,16,0"
|
||||
ItemsSource="{Binding Records}"
|
||||
SelectedItem="{Binding SelectedRecord}"
|
||||
AutoGenerateColumns="False"
|
||||
CanUserAddRows="False"
|
||||
CanUserDeleteRows="False"
|
||||
CanUserResizeRows="False"
|
||||
HeadersVisibility="Column"
|
||||
SelectionMode="Single"
|
||||
SelectionUnit="FullRow"
|
||||
IsReadOnly="True"
|
||||
GridLinesVisibility="Horizontal"
|
||||
HorizontalGridLinesBrush="#FFEDEFF2"
|
||||
VerticalGridLinesBrush="Transparent"
|
||||
Background="White"
|
||||
RowBackground="White"
|
||||
AlternatingRowBackground="#FAFCFF"
|
||||
RowHeight="32"
|
||||
ColumnHeaderHeight="34"
|
||||
RowHeaderWidth="0"
|
||||
MouseDoubleClick="WarehouseAreasGrid_MouseDoubleClick">
|
||||
<DataGrid.Resources>
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="#EAF3FF"/>
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="#1F1F1F"/>
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="#EAF3FF"/>
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}" Color="#1F1F1F"/>
|
||||
</DataGrid.Resources>
|
||||
<DataGrid.ColumnHeaderStyle>
|
||||
<Style TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Background" Value="#F5F7FA"/>
|
||||
<Setter Property="Foreground" Value="#606266"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Padding" Value="8,0"/>
|
||||
<Setter Property="BorderBrush" Value="#EBEEF5"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1"/>
|
||||
</Style>
|
||||
</DataGrid.ColumnHeaderStyle>
|
||||
<DataGrid.RowStyle>
|
||||
<Style TargetType="DataGridRow">
|
||||
<Setter Property="Background" Value="White"/>
|
||||
<Setter Property="Foreground" Value="#262626"/>
|
||||
<Setter Property="BorderBrush" Value="#FFEDEFF2"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="#EAF3FF"/>
|
||||
<Setter Property="Foreground" Value="#1F1F1F"/>
|
||||
<Setter Property="BorderBrush" Value="#D6E8FF"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#F5F9FF"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</DataGrid.RowStyle>
|
||||
<DataGrid.CellStyle>
|
||||
<Style TargetType="DataGridCell">
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="8,0"/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
</Style>
|
||||
</DataGrid.CellStyle>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="库区编码" Binding="{Binding AreaCode}" Width="130"/>
|
||||
<DataGridTextColumn Header="库区名称" Binding="{Binding AreaName}" Width="*"/>
|
||||
<DataGridTextColumn Header="所属仓库" Binding="{Binding WarehouseName}" Width="160"/>
|
||||
<DataGridTextColumn Header="仓库分类" Binding="{Binding WarehouseCategoryName}" Width="100"/>
|
||||
<DataGridTextColumn Header="状态" Binding="{Binding StatusText}" Width="70"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<!-- 底部 -->
|
||||
<Border Grid.Row="3" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,1,0,0"
|
||||
Padding="16,12">
|
||||
<Grid>
|
||||
<TextBlock VerticalAlignment="Center" FontSize="13" MaxWidth="420"
|
||||
TextTrimming="CharacterEllipsis" HorizontalAlignment="Left"
|
||||
Text="{Binding SelectedRecordDisplay}">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource SecondaryTextBrush}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasSelectedRecord}" Value="True">
|
||||
<Setter Property="Foreground" Value="{DynamicResource PrimaryBrush}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Content="取 消" Command="{Binding CancelCommand}"
|
||||
Style="{StaticResource ButtonDefault}" Width="88" Margin="0,0,12,0" Height="34"/>
|
||||
<Button Content="确认选择" Command="{Binding ConfirmCommand}"
|
||||
Style="{StaticResource ButtonPrimary}" Width="100" Height="34"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using YY.Admin.ViewModels.RawMaterialEntry;
|
||||
|
||||
namespace YY.Admin.Views.RawMaterialEntry;
|
||||
|
||||
public partial class WarehouseAreaPickerDialogView : UserControl
|
||||
{
|
||||
public WarehouseAreaPickerDialogView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 双击数据行 = 确认选择,提升使用效率。
|
||||
/// </summary>
|
||||
private void WarehouseAreasGrid_MouseDoubleClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (DataContext is WarehouseAreaPickerDialogViewModel vm && vm.ConfirmCommand.CanExecute())
|
||||
{
|
||||
vm.ConfirmCommand.Execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,9 +136,11 @@
|
||||
</DataGrid.CellStyle>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="榜单号" Binding="{Binding BillNo}" Width="180"/>
|
||||
<DataGridTextColumn Header="车牌号" Binding="{Binding PlateNumber}" Width="120"/>
|
||||
<DataGridTextColumn Header="车牌号" Binding="{Binding PlateNumber}" Width="100"/>
|
||||
<DataGridTextColumn Header="供应商(发货单位)" Binding="{Binding SenderUnit}" Width="*"/>
|
||||
<DataGridTextColumn Header="称重日期" Binding="{Binding WeighDate, StringFormat='yyyy-MM-dd'}" Width="120"/>
|
||||
<DataGridTextColumn Header="净重(KG)" Binding="{Binding NetWeight, StringFormat=N2}" Width="90"/>
|
||||
<DataGridTextColumn Header="已入场(KG)" Binding="{Binding EnteredWeight, StringFormat=N2}" Width="100"/>
|
||||
<DataGridTextColumn Header="称重日期" Binding="{Binding WeighDate, StringFormat='yyyy-MM-dd'}" Width="110"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<UserControl x:Class="YY.Admin.Views.WarehouseArea.WarehouseAreaEditDialogView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||
mc:Ignorable="d"
|
||||
Width="640"
|
||||
MinHeight="360">
|
||||
|
||||
<Grid Background="{DynamicResource ThirdlyRegionBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<hc:SimplePanel Margin="20">
|
||||
<TextBlock FontSize="18" Foreground="{DynamicResource PrimaryTextBrush}" Text="{Binding DialogTitle}" HorizontalAlignment="Left" VerticalAlignment="Top"/>
|
||||
<Button Width="22" Height="22" Command="hc:ControlCommands.Close" Style="{StaticResource ButtonIcon}"
|
||||
Foreground="{DynamicResource PrimaryBrush}" hc:IconElement.Geometry="{StaticResource ErrorGeometry}"
|
||||
Padding="0" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,4,4,0"/>
|
||||
</hc:SimplePanel>
|
||||
|
||||
<!-- 表单 -->
|
||||
<hc:ScrollViewer Grid.Row="1" IsInertiaEnabled="True">
|
||||
<StackPanel Margin="20,0,20,0">
|
||||
<hc:Row Gutter="10">
|
||||
|
||||
<!-- 库区编码 -->
|
||||
<hc:Col Span="12">
|
||||
<hc:TextBox Text="{Binding Area.AreaCode, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Title="库区编码"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入库区编码"
|
||||
hc:InfoElement.Necessary="True"
|
||||
hc:InfoElement.Symbol="*"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,16"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 库区名称 -->
|
||||
<hc:Col Span="12">
|
||||
<hc:TextBox Text="{Binding Area.AreaName, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Title="库区名称"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="默认同库区编码"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,16"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 所属仓库名称(只读显示) -->
|
||||
<hc:Col Span="12">
|
||||
<hc:TextBox Text="{Binding Area.WarehouseName}"
|
||||
hc:InfoElement.Title="所属仓库"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
IsReadOnly="True"
|
||||
Margin="0,0,0,16"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 仓库分类(只读,由仓库带出) -->
|
||||
<hc:Col Span="12">
|
||||
<hc:TextBox Text="{Binding Area.WarehouseCategoryName}"
|
||||
hc:InfoElement.Title="仓库分类"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
IsReadOnly="True"
|
||||
Margin="0,0,0,16"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 最大存放量 -->
|
||||
<hc:Col Span="12">
|
||||
<hc:NumericUpDown Value="{Binding Area.MaxCapacity}"
|
||||
Minimum="0"
|
||||
DecimalPlaces="0"
|
||||
Style="{StaticResource NumericUpDownPlus}"
|
||||
hc:InfoElement.Title="最大存放量"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入最大存放量"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,16"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 实际存放量 -->
|
||||
<hc:Col Span="12">
|
||||
<hc:NumericUpDown Value="{Binding Area.ActualCapacity}"
|
||||
Minimum="0"
|
||||
DecimalPlaces="0"
|
||||
Style="{StaticResource NumericUpDownPlus}"
|
||||
hc:InfoElement.Title="实际存放量"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入实际存放量"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,16"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 状态 -->
|
||||
<hc:Col Span="12">
|
||||
<hc:ComboBox hc:InfoElement.Title="状态"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
SelectedValue="{Binding Area.Status}"
|
||||
SelectedValuePath="Tag"
|
||||
Margin="0,0,0,16">
|
||||
<ComboBoxItem Content="启用" Tag="0"/>
|
||||
<ComboBoxItem Content="停用" Tag="1"/>
|
||||
</hc:ComboBox>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 备注 -->
|
||||
<hc:Col Span="24">
|
||||
<hc:TextBox Text="{Binding Area.Remark, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Title="备注"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入备注"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
TextWrapping="Wrap"
|
||||
AcceptsReturn="True"
|
||||
Height="80"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
Margin="0,0,0,16"/>
|
||||
</hc:Col>
|
||||
|
||||
</hc:Row>
|
||||
</StackPanel>
|
||||
</hc:ScrollViewer>
|
||||
|
||||
<!-- 按钮区 -->
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center" Margin="20">
|
||||
<Button Content="取消" Command="{Binding CancelCommand}" Style="{StaticResource ButtonDefault}" Margin="0,0,15,0" Width="100"/>
|
||||
<Button Content="确定" Command="{Binding SaveCommand}" Style="{StaticResource ButtonPrimary}" Width="100"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace YY.Admin.Views.WarehouseArea;
|
||||
|
||||
public partial class WarehouseAreaEditDialogView : UserControl
|
||||
{
|
||||
public WarehouseAreaEditDialogView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<UserControl x:Class="YY.Admin.Views.WarehouseArea.WarehouseAreaListView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid Style="{StaticResource BaseViewStyle}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 搜索条件 -->
|
||||
<Border Grid.Row="0" CornerRadius="4" Margin="0 0 -10 0">
|
||||
<hc:Row>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
|
||||
<hc:TextBox Text="{Binding FilterAreaCode, UpdateSourceTrigger=PropertyChanged}"
|
||||
Margin="0 0 10 10"
|
||||
hc:InfoElement.Title="库区编码"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.TitleWidth="65"
|
||||
hc:InfoElement.Placeholder="请输入库区编码"
|
||||
hc:InfoElement.ShowClearButton="True"/>
|
||||
</hc:Col>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
|
||||
<hc:TextBox Text="{Binding FilterAreaName, UpdateSourceTrigger=PropertyChanged}"
|
||||
Margin="0 0 10 10"
|
||||
hc:InfoElement.Title="库区名称"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.TitleWidth="65"
|
||||
hc:InfoElement.Placeholder="请输入库区名称"
|
||||
hc:InfoElement.ShowClearButton="True"/>
|
||||
</hc:Col>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
|
||||
<hc:ComboBox SelectedValuePath="Value"
|
||||
DisplayMemberPath="Key"
|
||||
ItemsSource="{Binding WarehouseOptions}"
|
||||
SelectedValue="{Binding FilterWarehouseId}"
|
||||
Margin="0 0 10 10"
|
||||
hc:InfoElement.Title="所属仓库"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.TitleWidth="65"
|
||||
hc:InfoElement.Placeholder="请选择仓库"
|
||||
hc:InfoElement.ShowClearButton="True"/>
|
||||
</hc:Col>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
|
||||
<hc:ComboBox SelectedValuePath="Value"
|
||||
DisplayMemberPath="Key"
|
||||
ItemsSource="{Binding StatusOptions}"
|
||||
SelectedValue="{Binding FilterStatus}"
|
||||
Margin="0 0 10 10"
|
||||
hc:InfoElement.Title="状态"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.TitleWidth="65"
|
||||
hc:InfoElement.Placeholder="请选择状态"
|
||||
hc:InfoElement.ShowClearButton="True"/>
|
||||
</hc:Col>
|
||||
</hc:Row>
|
||||
</Border>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<Border Grid.Row="1" Margin="0,10">
|
||||
<hc:UniformSpacingPanel Spacing="10">
|
||||
<Button Style="{StaticResource ButtonPrimary}" Command="{Binding SearchCommand}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="Search"/>
|
||||
<TextBlock Text="搜索" Style="{StaticResource IconButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ButtonDefault}" Command="{Binding ResetCommand}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="Refresh"/>
|
||||
<TextBlock Text="重置" Style="{StaticResource IconButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ButtonPrimary}" Command="{Binding AddCommand}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="Plus"/>
|
||||
<TextBlock Text="新增" Style="{StaticResource IconButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</hc:UniformSpacingPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<DataGrid Grid.Row="2"
|
||||
ItemsSource="{Binding Areas}"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
CanUserAddRows="False"
|
||||
SelectionMode="Extended"
|
||||
SelectionUnit="FullRow"
|
||||
RowHeaderWidth="55"
|
||||
GridLinesVisibility="Horizontal"
|
||||
HorizontalGridLinesBrush="#FFEDEDED"
|
||||
VerticalGridLinesBrush="Transparent"
|
||||
HeadersVisibility="All"
|
||||
ColumnHeaderStyle="{StaticResource CusDataGridColumnHeaderStyle}"
|
||||
Style="{StaticResource CusDataGridStyle}"
|
||||
hc:DataGridAttach.ShowSelectAllButton="True"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Auto">
|
||||
<DataGrid.RowHeaderTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=DataGridRow}}"/>
|
||||
</DataTemplate>
|
||||
</DataGrid.RowHeaderTemplate>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="库区编码" Binding="{Binding AreaCode}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="120"/>
|
||||
<DataGridTextColumn Header="库区名称" Binding="{Binding AreaName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="160"/>
|
||||
<DataGridTextColumn Header="所属仓库" Binding="{Binding WarehouseName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="160"/>
|
||||
<DataGridTextColumn Header="仓库分类" Binding="{Binding WarehouseCategoryName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="120"/>
|
||||
<DataGridTextColumn Header="最大存放量" Binding="{Binding MaxCapacity}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="100"/>
|
||||
<DataGridTextColumn Header="实际存放量" Binding="{Binding ActualCapacity}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="100"/>
|
||||
<DataGridTextColumn Header="备注" Binding="{Binding Remark}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="180"/>
|
||||
<DataGridTextColumn Header="状态" Binding="{Binding StatusText}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="70"/>
|
||||
<DataGridTextColumn Header="创建时间" Binding="{Binding CreateTime, StringFormat=yyyy-MM-dd HH:mm}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="140"/>
|
||||
<DataGridTemplateColumn Header="操作" Width="160" IsReadOnly="True">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<hc:UniformSpacingPanel Spacing="6">
|
||||
<Button Content="编辑" Style="{StaticResource ButtonInfo}"
|
||||
Command="{Binding DataContext.EditCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||
CommandParameter="{Binding}"
|
||||
Padding="8,2"/>
|
||||
<Button Style="{StaticResource ButtonWarning}"
|
||||
Content="{Binding StatusText}"
|
||||
Command="{Binding DataContext.ToggleStatusCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||
CommandParameter="{Binding}"
|
||||
Padding="8,2"/>
|
||||
<Button Content="删除" Style="{StaticResource ButtonDanger}"
|
||||
Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||
CommandParameter="{Binding}"
|
||||
Padding="8,2"/>
|
||||
</hc:UniformSpacingPanel>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<!-- 分页 -->
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
|
||||
<TextBlock Text="{Binding Total, StringFormat=共 {0} 条}" VerticalAlignment="Center" Margin="0,0,16,0"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"/>
|
||||
<Button Content="上一页" Command="{Binding PrevPageCommand}" Style="{StaticResource ButtonDefault}" Margin="0,0,4,0" Width="80"/>
|
||||
<TextBlock Text="{Binding PageNo, StringFormat=第 {0} 页}" VerticalAlignment="Center" Margin="8,0"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"/>
|
||||
<Button Content="下一页" Command="{Binding NextPageCommand}" Style="{StaticResource ButtonDefault}" Width="80"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace YY.Admin.Views.WarehouseArea;
|
||||
|
||||
public partial class WarehouseAreaListView : UserControl
|
||||
{
|
||||
public WarehouseAreaListView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -147,6 +147,7 @@
|
||||
<DataGridTextColumn Header="毛重(KG)" Binding="{Binding GrossWeight, StringFormat=N2}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="100"/>
|
||||
<DataGridTextColumn Header="皮重(KG)" Binding="{Binding TareWeight, StringFormat=N2}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="100"/>
|
||||
<DataGridTextColumn Header="净重(KG)" Binding="{Binding NetWeight, StringFormat=N2}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="100"/>
|
||||
<DataGridTextColumn Header="已入场重量(KG)" Binding="{Binding EnteredWeight, StringFormat=N2}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="130"/>
|
||||
<DataGridTextColumn Header="司机" Binding="{Binding DriverName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="90"/>
|
||||
<DataGridTextColumn Header="手机号" Binding="{Binding DriverPhone}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="120"/>
|
||||
<DataGridTextColumn Header="创建时间" Binding="{Binding CreateTime, StringFormat='yyyy-MM-dd HH:mm'}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="130"/>
|
||||
|
||||
Reference in New Issue
Block a user