diff --git a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/BrowserUtils.java b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/BrowserUtils.java index 479a917..4d12ea5 100644 --- a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/BrowserUtils.java +++ b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/BrowserUtils.java @@ -203,7 +203,16 @@ public class BrowserUtils { /** 判断请求是否来自移动端 */ public static boolean isMobile(HttpServletRequest request) { - String ua = request.getHeader("User-Agent").toLowerCase(); + // 某些场景(如服务端内部 HttpClient、监控探针、客户端在响应前断开连接)请求不带 User-Agent, + // 必须先做 null/空判断,否则会在全局异常处理器中触发二次 NPE,掩盖原始异常并污染日志。 + if (request == null) { + return false; + } + String header = request.getHeader("User-Agent"); + if (header == null || header.isEmpty()) { + return false; + } + String ua = header.toLowerCase(); String type = "(phone|pad|pod|iphone|ipod|ios|ipad|android|mobile|blackberry|iemobile|mqqbrowser|juc|fennec|wosbrowser|browserng|webos|symbian|windows phone)"; Pattern pattern = Pattern.compile(type); return pattern.matcher(ua).find(); diff --git a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java index 5b7c808..56d6d50 100644 --- a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java +++ b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java @@ -205,6 +205,8 @@ public class ShiroConfig { filterChainDefinitionMap.put("/xslmes/mesXslWeightRecord/anon/**", "anon"); // MES原料入场记录免密接口(供桌面端调用) filterChainDefinitionMap.put("/xslmes/mesXslRawMaterialEntry/anon/**", "anon"); + // MES原材料卡片免密接口(供桌面端调用) + filterChainDefinitionMap.put("/xslmes/mesXslRawMaterialCard/anon/**", "anon"); // MES密炼物料管理免密接口(供桌面端调用) filterChainDefinitionMap.put("/mes/material/mixerMaterial/anon/**", "anon"); // 系统分类字典免密接口(供桌面端调用) diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java index f8f31fc..accbd7e 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java @@ -14,11 +14,13 @@ import org.jeecg.common.system.query.QueryGenerator; import org.jeecg.common.util.oConvertUtils; import org.jeecg.modules.xslmes.constant.MesXslCustomerBizStatus; import org.jeecg.modules.xslmes.entity.MesXslCustomer; +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.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; @@ -49,6 +51,7 @@ public class MesXslDesktopAnonController { private final IMesXslSupplierService supplierService; private final IMesXslWeightRecordService weightRecordService; private final IMesXslRawMaterialEntryService rawMaterialEntryService; + private final IMesXslRawMaterialCardService rawMaterialCardService; private final MesXslStompNotifyService stompNotify; // ═══════════════════════════ 车辆管理 ═══════════════════════════ @@ -502,6 +505,66 @@ public class MesXslDesktopAnonController { return Result.OK("批量删除成功!"); } + // ═══════════════════════════ 原材料卡片 ═══════════════════════════ + + @Operation(summary = "原材料卡片-免密分页列表查询") + @GetMapping("/xslmes/mesXslRawMaterialCard/anon/list") + public Result> rawMaterialCardAnonList( + MesXslRawMaterialCard entity, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + HttpServletRequest req) { + QueryWrapper qw = QueryGenerator.initQueryWrapper(entity, req.getParameterMap()); + qw.orderByDesc("create_time"); + IPage page = rawMaterialCardService.page(new Page<>(pageNo, pageSize), qw); + return Result.OK(page); + } + + @Operation(summary = "原材料卡片-免密通过id查询") + @GetMapping("/xslmes/mesXslRawMaterialCard/anon/queryById") + public Result rawMaterialCardAnonQueryById(@RequestParam(name = "id") String id) { + MesXslRawMaterialCard entity = rawMaterialCardService.getById(id); + return entity != null ? Result.OK(entity) : Result.error("未找到对应数据"); + } + + @Operation(summary = "原材料卡片-免密添加") + @PostMapping("/xslmes/mesXslRawMaterialCard/anon/add") + public Result rawMaterialCardAnonAdd(@RequestBody MesXslRawMaterialCard entity) { + rawMaterialCardService.save(entity); + stompNotify.publishRawMaterialCardChanged("add", entity.getId()); + return Result.OK("添加成功!"); + } + + @Operation(summary = "原材料卡片-免密编辑") + @RequestMapping(value = "/xslmes/mesXslRawMaterialCard/anon/edit", method = {RequestMethod.PUT, RequestMethod.POST}) + public Result rawMaterialCardAnonEdit(@RequestBody MesXslRawMaterialCard entity) { + if (oConvertUtils.isEmpty(entity.getId())) { + return Result.error("主键不能为空"); + } + boolean ok = rawMaterialCardService.updateById(entity); + if (!ok) { + return Result.error("数据已被他人修改,请刷新后重试"); + } + stompNotify.publishRawMaterialCardChanged("edit", entity.getId()); + return Result.OK("编辑成功!"); + } + + @Operation(summary = "原材料卡片-免密删除") + @DeleteMapping("/xslmes/mesXslRawMaterialCard/anon/delete") + public Result rawMaterialCardAnonDelete(@RequestParam(name = "id") String id) { + rawMaterialCardService.removeById(id); + stompNotify.publishRawMaterialCardChanged("delete", id); + return Result.OK("删除成功!"); + } + + @Operation(summary = "原材料卡片-免密批量删除") + @DeleteMapping("/xslmes/mesXslRawMaterialCard/anon/deleteBatch") + public Result rawMaterialCardAnonDeleteBatch(@RequestParam(name = "ids") String ids) { + rawMaterialCardService.removeByIds(Arrays.asList(ids.split(","))); + stompNotify.publishRawMaterialCardChanged("batchDelete", ids); + return Result.OK("批量删除成功!"); + } + // ─────────────────────────── 车辆私有辅助 ──────────────────────────── private void applyWeightBillType(MesXslWeightRecord record) { diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRawMaterialCardController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRawMaterialCardController.java new file mode 100644 index 0000000..144d131 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRawMaterialCardController.java @@ -0,0 +1,155 @@ +package org.jeecg.modules.xslmes.controller; + +import java.util.Arrays; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jeecg.common.api.vo.Result; +import org.jeecg.common.system.query.QueryGenerator; +import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard; +import org.jeecg.modules.xslmes.service.IMesXslRawMaterialCardService; +import org.jeecg.modules.xslmes.service.MesXslStompNotifyService; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; + +import org.jeecg.common.system.base.controller.JeecgController; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.jeecg.common.aspect.annotation.AutoLog; +import org.apache.shiro.authz.annotation.RequiresPermissions; + +/** + * @Description: 原材料卡片 + * @Author: jeecg-boot + * @Date: 2026-05-11 + * @Version: V1.0 + */ +@Tag(name = "原材料卡片") +@RestController +@RequestMapping("/xslmes/mesXslRawMaterialCard") +@Slf4j +public class MesXslRawMaterialCardController extends JeecgController { + @Autowired + private IMesXslRawMaterialCardService mesXslRawMaterialCardService; + @Autowired + private MesXslStompNotifyService stompNotify; + + /** + * 分页列表查询 + */ + @Operation(summary = "原材料卡片-分页列表查询") + @GetMapping(value = "/list") + public Result> queryPageList(MesXslRawMaterialCard mesXslRawMaterialCard, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + HttpServletRequest req) { + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(mesXslRawMaterialCard, req.getParameterMap()); + Page page = new Page<>(pageNo, pageSize); + IPage pageList = mesXslRawMaterialCardService.page(page, queryWrapper); + return Result.OK(pageList); + } + + /** + * 添加 + */ + @AutoLog(value = "原材料卡片-添加") + @Operation(summary = "原材料卡片-添加") + @RequiresPermissions("xslmes:mes_xsl_raw_material_card:add") + @PostMapping(value = "/add") + public Result add(@RequestBody MesXslRawMaterialCard mesXslRawMaterialCard) { + mesXslRawMaterialCardService.save(mesXslRawMaterialCard); + stompNotify.publishRawMaterialCardChanged("add", mesXslRawMaterialCard.getId()); + return Result.OK("添加成功!"); + } + + /** + * 编辑 + */ + @AutoLog(value = "原材料卡片-编辑") + @Operation(summary = "原材料卡片-编辑") + @RequiresPermissions("xslmes:mes_xsl_raw_material_card:edit") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST}) + public Result edit(@RequestBody MesXslRawMaterialCard mesXslRawMaterialCard) { + mesXslRawMaterialCardService.updateById(mesXslRawMaterialCard); + stompNotify.publishRawMaterialCardChanged("edit", mesXslRawMaterialCard.getId()); + return Result.OK("编辑成功!"); + } + + /** + * 设置优先出库(列表可直接切换) + */ + @AutoLog(value = "原材料卡片-设置优先出库") + @Operation(summary = "原材料卡片-设置优先出库") + @RequiresPermissions("xslmes:mes_xsl_raw_material_card:edit") + @PutMapping(value = "/updatePriority") + public Result updatePriority(@RequestParam String id, @RequestParam String priorityPickup) { + MesXslRawMaterialCard card = new MesXslRawMaterialCard(); + card.setId(id); + card.setPriorityPickup(priorityPickup); + mesXslRawMaterialCardService.updateById(card); + stompNotify.publishRawMaterialCardChanged("edit", id); + return Result.OK("更新成功!"); + } + + /** + * 通过id删除 + */ + @AutoLog(value = "原材料卡片-通过id删除") + @Operation(summary = "原材料卡片-通过id删除") + @RequiresPermissions("xslmes:mes_xsl_raw_material_card:delete") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name = "id", required = true) String id) { + mesXslRawMaterialCardService.removeById(id); + stompNotify.publishRawMaterialCardChanged("delete", id); + return Result.OK("删除成功!"); + } + + /** + * 批量删除 + */ + @AutoLog(value = "原材料卡片-批量删除") + @Operation(summary = "原材料卡片-批量删除") + @RequiresPermissions("xslmes:mes_xsl_raw_material_card:deleteBatch") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name = "ids", required = true) String ids) { + this.mesXslRawMaterialCardService.removeByIds(Arrays.asList(ids.split(","))); + stompNotify.publishRawMaterialCardChanged("batchDelete", ids); + return Result.OK("批量删除成功!"); + } + + /** + * 通过id查询 + */ + @Operation(summary = "原材料卡片-通过id查询") + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name = "id", required = true) String id) { + MesXslRawMaterialCard mesXslRawMaterialCard = mesXslRawMaterialCardService.getById(id); + if (mesXslRawMaterialCard == null) { + return Result.error("未找到对应数据"); + } + return Result.OK(mesXslRawMaterialCard); + } + + /** + * 导出excel + */ + @RequiresPermissions("xslmes:mes_xsl_raw_material_card:exportXls") + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, MesXslRawMaterialCard mesXslRawMaterialCard) { + return super.exportXls(request, mesXslRawMaterialCard, MesXslRawMaterialCard.class, "原材料卡片"); + } + + /** + * 通过excel导入数据 + */ + @RequiresPermissions("xslmes:mes_xsl_raw_material_card:importExcel") + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + return super.importExcel(request, response, MesXslRawMaterialCard.class); + } +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialCard.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialCard.java new file mode 100644 index 0000000..54d020d --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialCard.java @@ -0,0 +1,133 @@ +package org.jeecg.modules.xslmes.entity; + +import java.io.Serializable; +import java.util.Date; +import java.math.BigDecimal; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.jeecg.common.aspect.annotation.Dict; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * @Description: 原材料卡片 + * @Author: jeecg-boot + * @Date: 2026-05-11 + * @Version: V1.0 + */ +@Data +@TableName("mes_xsl_raw_material_card") +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +@Schema(description = "原材料卡片") +public class MesXslRawMaterialCard implements Serializable { + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "主键") + private String id; + + @Excel(name = "条码", width = 20) + @Schema(description = "条码") + private String barcode; + + @Excel(name = "批次号", width = 20) + @Schema(description = "批次号") + private String batchNo; + + @Excel(name = "入场日期", width = 15, format = "yyyy-MM-dd") + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern = "yyyy-MM-dd") + @Schema(description = "入场日期") + private Date entryDate; + + @Schema(description = "物料ID") + private String materialId; + + @Excel(name = "物料名称", width = 20) + @Schema(description = "物料名称") + private String materialName; + + @Excel(name = "物料描述", width = 30) + @Schema(description = "物料描述") + private String materialDesc; + + @Schema(description = "供应商ID") + private String supplierId; + + @Excel(name = "供应商名称", width = 20) + @Schema(description = "供应商名称") + private String supplierName; + + @Excel(name = "厂家物料名称", width = 20) + @Schema(description = "厂家物料名称") + private String manufacturerMaterialName; + + @Excel(name = "保质期", width = 15) + @Schema(description = "保质期") + private String shelfLife; + + @Excel(name = "总重", width = 12) + @Schema(description = "总重") + private BigDecimal totalWeight; + + @Excel(name = "剩余重量", width = 12) + @Schema(description = "剩余重量") + private BigDecimal remainingWeight; + + @Excel(name = "剩余数量", width = 12) + @Schema(description = "剩余数量") + private Integer remainingQuantity; + + @Excel(name = "状态", width = 10, dicCode = "xslmes_card_status") + @Dict(dicCode = "xslmes_card_status") + @Schema(description = "状态(xslmes_card_status:1正常 0异常)") + private String status; + + @Excel(name = "检测结果", width = 12, dicCode = "xslmes_test_result") + @Dict(dicCode = "xslmes_test_result") + @Schema(description = "检测结果(xslmes_test_result:0未检 1合格 2不合格)") + private String testResult; + + @Excel(name = "库区", width = 15) + @Schema(description = "库区") + private String warehouseArea; + + @Excel(name = "卸货人", width = 15) + @Schema(description = "卸货人") + private String unloadOperator; + + @Excel(name = "优先出库", width = 10, dicCode = "yn") + @Dict(dicCode = "yn") + @Schema(description = "设置优先出库(yn:1是 0否)") + private String priorityPickup; + + /**创建人*/ + @Schema(description = "创建人") + private String createBy; + + /**创建日期*/ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "创建日期") + private Date createTime; + + /**更新人*/ + @Schema(description = "更新人") + private String updateBy; + + /**更新日期*/ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "更新日期") + private Date updateTime; + + @Schema(description = "租户ID") + private Integer tenantId; +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialEntry.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialEntry.java index 44412ba..053dba6 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialEntry.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialEntry.java @@ -1,8 +1,8 @@ package org.jeecg.modules.xslmes.entity; import java.io.Serializable; -import java.util.Date; import java.math.BigDecimal; +import java.util.Date; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; @@ -88,17 +88,19 @@ public class MesXslRawMaterialEntry implements Serializable { @Schema(description = "总重(KG)") private BigDecimal totalWeight; - @Excel(name = "总份数", width = 10) - @Schema(description = "总份数") - private Integer totalPortions; + // 总份数 / 每份总重 / 每份包数:从 数值类型 升级为 字符串类型, + // 支持桌面端「拆码明细」多行拼接保存(如 20/1/ 与 100/200/)。 + @Excel(name = "总份数", width = 12) + @Schema(description = "总份数(支持多行拆码明细拼接,如 20/1/)") + private String totalPortions; - @Excel(name = "每份总重(KG)", width = 12) - @Schema(description = "每份总重(KG)") - private BigDecimal portionWeight; + @Excel(name = "每份总重(KG)", width = 14) + @Schema(description = "每份总重(KG)(支持多行拆码明细拼接,如 100/200/)") + private String portionWeight; - @Excel(name = "每份包数", width = 10) - @Schema(description = "每份包数") - private Integer portionPackages; + @Excel(name = "每份包数", width = 12) + @Schema(description = "每份包数(支持多行拆码明细拼接)") + private String portionPackages; @Excel(name = "检测结果", width = 12, dicCode = "xslmes_test_result") @Dict(dicCode = "xslmes_test_result") diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslRawMaterialCardMapper.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslRawMaterialCardMapper.java new file mode 100644 index 0000000..7110eeb --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslRawMaterialCardMapper.java @@ -0,0 +1,13 @@ +package org.jeecg.modules.xslmes.mapper; + +import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: 原材料卡片 + * @Author: jeecg-boot + * @Date: 2026-05-11 + * @Version: V1.0 + */ +public interface MesXslRawMaterialCardMapper extends BaseMapper { +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/xml/MesXslRawMaterialCardMapper.xml b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/xml/MesXslRawMaterialCardMapper.xml new file mode 100644 index 0000000..f240dd2 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/xml/MesXslRawMaterialCardMapper.xml @@ -0,0 +1,4 @@ + + + + diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRawMaterialCardService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRawMaterialCardService.java new file mode 100644 index 0000000..97ac1cd --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRawMaterialCardService.java @@ -0,0 +1,13 @@ +package org.jeecg.modules.xslmes.service; + +import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + * @Description: 原材料卡片 + * @Author: jeecg-boot + * @Date: 2026-05-11 + * @Version: V1.0 + */ +public interface IMesXslRawMaterialCardService extends IService { +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/MesXslStompNotifyService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/MesXslStompNotifyService.java index aa8320e..5bbe5ab 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/MesXslStompNotifyService.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/MesXslStompNotifyService.java @@ -55,6 +55,11 @@ public class MesXslStompNotifyService { publish("/topic/sync/sys-categories", "SYS_CATEGORY_CHANGED", "categoryId", categoryId, action); } + /** 广播原材料卡片数据变更事件到 /topic/sync/mes-raw-material-cards */ + public void publishRawMaterialCardChanged(String action, String cardId) { + publish("/topic/sync/mes-raw-material-cards", "RAW_MATERIAL_CARD_CHANGED", "cardId", cardId, action); + } + /** 广播数据字典变更事件到 /topic/sync/sys-dicts */ public void publishSysDictChanged(String action, String dictId) { publish("/topic/sync/sys-dicts", "SYS_DICT_CHANGED", "dictId", dictId, action); diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRawMaterialCardServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRawMaterialCardServiceImpl.java new file mode 100644 index 0000000..0a308ab --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRawMaterialCardServiceImpl.java @@ -0,0 +1,17 @@ +package org.jeecg.modules.xslmes.service.impl; + +import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard; +import org.jeecg.modules.xslmes.mapper.MesXslRawMaterialCardMapper; +import org.jeecg.modules.xslmes.service.IMesXslRawMaterialCardService; +import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +/** + * @Description: 原材料卡片 + * @Author: jeecg-boot + * @Date: 2026-05-11 + * @Version: V1.0 + */ +@Service +public class MesXslRawMaterialCardServiceImpl extends ServiceImpl implements IMesXslRawMaterialCardService { +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_43__mes_xsl_raw_material_entry_portion_to_varchar.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_43__mes_xsl_raw_material_entry_portion_to_varchar.sql new file mode 100644 index 0000000..1c12526 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_43__mes_xsl_raw_material_entry_portion_to_varchar.sql @@ -0,0 +1,45 @@ +-- ============================================================ +-- 原料入场记录表:「总份数 / 每份总重(KG) / 每份包数」由数值类型升级为字符串类型 +-- 背景:桌面端「拆码明细」支持多行(如 20/1,100/200),需要把拼接结果整体保存。 +-- 原始类型:total_portions int、portion_weight decimal(10,2)、portion_packages int +-- 目标类型:均为 varchar(64) +-- ============================================================ + +-- 1) total_portions → varchar(64) +SET @col_type := ( + SELECT DATA_TYPE FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'mes_xsl_raw_material_entry' + AND COLUMN_NAME = 'total_portions' +); +SET @ddl := IF(@col_type IS NULL OR @col_type IN ('varchar','char','text'), + 'SELECT 1', + 'ALTER TABLE `mes_xsl_raw_material_entry` MODIFY COLUMN `total_portions` varchar(64) DEFAULT NULL COMMENT ''总份数(支持拆码明细多行拼接,如 20/1/)''' +); +PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; + +-- 2) portion_weight → varchar(64) +SET @col_type := ( + SELECT DATA_TYPE FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'mes_xsl_raw_material_entry' + AND COLUMN_NAME = 'portion_weight' +); +SET @ddl := IF(@col_type IS NULL OR @col_type IN ('varchar','char','text'), + 'SELECT 1', + 'ALTER TABLE `mes_xsl_raw_material_entry` MODIFY COLUMN `portion_weight` varchar(64) DEFAULT NULL COMMENT ''每份总重(KG)(支持拆码明细多行拼接,如 100/200/)''' +); +PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; + +-- 3) portion_packages → varchar(64) +SET @col_type := ( + SELECT DATA_TYPE FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'mes_xsl_raw_material_entry' + AND COLUMN_NAME = 'portion_packages' +); +SET @ddl := IF(@col_type IS NULL OR @col_type IN ('varchar','char','text'), + 'SELECT 1', + 'ALTER TABLE `mes_xsl_raw_material_entry` MODIFY COLUMN `portion_packages` varchar(64) DEFAULT NULL COMMENT ''每份包数(支持拆码明细多行拼接)''' +); +PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_44__mes_xsl_raw_material_card.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_44__mes_xsl_raw_material_card.sql new file mode 100644 index 0000000..68fb8c0 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_44__mes_xsl_raw_material_card.sql @@ -0,0 +1,121 @@ +-- 原材料卡片:建表 + 字典 + 菜单权限(幂等) + +-- ===================== 1. 建表 ===================== +CREATE TABLE IF NOT EXISTS `mes_xsl_raw_material_card` ( + `id` varchar(32) NOT NULL COMMENT '主键', + `barcode` varchar(100) DEFAULT NULL COMMENT '条码', + `batch_no` varchar(100) DEFAULT NULL COMMENT '批次号', + `entry_date` date DEFAULT NULL COMMENT '入场日期', + `material_id` varchar(32) DEFAULT NULL COMMENT '物料ID', + `material_name` varchar(200) DEFAULT NULL COMMENT '物料名称', + `material_desc` text COMMENT '物料描述', + `supplier_id` varchar(32) DEFAULT NULL COMMENT '供应商ID', + `supplier_name` varchar(200) DEFAULT NULL COMMENT '供应商名称', + `manufacturer_material_name` varchar(200) DEFAULT NULL COMMENT '厂家物料名称', + `shelf_life` varchar(50) DEFAULT NULL COMMENT '保质期', + `total_weight` decimal(12,3) DEFAULT NULL COMMENT '总重', + `remaining_weight` decimal(12,3) DEFAULT NULL COMMENT '剩余重量', + `remaining_quantity` int DEFAULT NULL COMMENT '剩余数量', + `status` varchar(10) DEFAULT NULL COMMENT '状态(字典 xslmes_card_status:1正常 0异常)', + `test_result` varchar(10) DEFAULT NULL COMMENT '检测结果(字典 xslmes_test_result:0未检 1合格 2不合格)', + `warehouse_area` varchar(100) DEFAULT NULL COMMENT '库区', + `unload_operator` varchar(100) DEFAULT NULL COMMENT '卸货人', + `priority_pickup` varchar(2) DEFAULT '0' COMMENT '设置优先出库(字典 yn:1是 0否)', + `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 '更新时间', + `tenant_id` int DEFAULT 1002 COMMENT '租户ID', + PRIMARY KEY (`id`), + KEY `idx_rmc_barcode` (`barcode`), + KEY `idx_rmc_batch_no` (`batch_no`), + KEY `idx_rmc_entry_date` (`entry_date`), + KEY `idx_rmc_material_id` (`material_id`), + KEY `idx_rmc_supplier_id` (`supplier_id`), + KEY `idx_rmc_status` (`status`), + KEY `idx_rmc_priority` (`priority_pickup`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='原材料卡片'; + +-- ===================== 2. 状态字典(xslmes_card_status)===================== +INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`) +SELECT REPLACE(UUID(), '-', ''), 'MES卡片状态', 'xslmes_card_status', '原材料卡片状态:正常/异常', 0, 'admin', NOW(), 0, 1002 +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_card_status' AND `del_flag` = 0); + +-- 字典项:正常 +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`) +SELECT REPLACE(UUID(), '-', ''), d.id, '正常', '1', '正常', 1, 1, 'admin', NOW() +FROM `sys_dict` d +WHERE d.`dict_code` = 'xslmes_card_status' + AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = '1'); + +-- 字典项:异常 +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`) +SELECT REPLACE(UUID(), '-', ''), d.id, '异常', '0', '异常', 2, 1, 'admin', NOW() +FROM `sys_dict` d +WHERE d.`dict_code` = 'xslmes_card_status' + AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = '0'); + +-- ===================== 3. 菜单权限(父菜单: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 '1900000000000000540', '1900000000000000300', '原材料卡片', '/xslmes/mesXslRawMaterialCard', 'xslmes/mesXslRawMaterialCard/MesXslRawMaterialCardList', 1, NULL, NULL, 1, NULL, '0', 12.00, 0, 'ant-design:credit-card-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` = '1900000000000000540'); + +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 '1900000000000000541', '1900000000000000540', '添加', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_raw_material_card: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` = '1900000000000000541'); + +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 '1900000000000000542', '1900000000000000540', '编辑', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_raw_material_card: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` = '1900000000000000542'); + +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 '1900000000000000543', '1900000000000000540', '删除', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_raw_material_card: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` = '1900000000000000543'); + +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 '1900000000000000544', '1900000000000000540', '批量删除', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_raw_material_card: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` = '1900000000000000544'); + +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 '1900000000000000545', '1900000000000000540', '导出', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_raw_material_card: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` = '1900000000000000545'); + +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 '1900000000000000546', '1900000000000000540', '导入', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_raw_material_card: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` = '1900000000000000546'); + +-- ===================== 4. 角色菜单授权(admin 角色)===================== +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) +SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000540', 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` = '1900000000000000540'); + +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) +SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000541', 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` = '1900000000000000541'); + +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) +SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000542', 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` = '1900000000000000542'); + +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) +SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000543', 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` = '1900000000000000543'); + +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) +SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000544', 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` = '1900000000000000544'); + +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) +SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000545', 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` = '1900000000000000545'); + +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) +SELECT REPLACE(UUID(), '-', ''), r.id, '1900000000000000546', 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` = '1900000000000000546'); diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCard.api.ts b/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCard.api.ts new file mode 100644 index 0000000..26456ea --- /dev/null +++ b/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCard.api.ts @@ -0,0 +1,49 @@ +import { defHttp } from '/@/utils/http/axios'; +import { useMessage } from '/@/hooks/web/useMessage'; + +const { createConfirm } = useMessage(); + +enum Api { + list = '/xslmes/mesXslRawMaterialCard/list', + save = '/xslmes/mesXslRawMaterialCard/add', + edit = '/xslmes/mesXslRawMaterialCard/edit', + deleteOne = '/xslmes/mesXslRawMaterialCard/delete', + deleteBatch = '/xslmes/mesXslRawMaterialCard/deleteBatch', + importExcel = '/xslmes/mesXslRawMaterialCard/importExcel', + exportXls = '/xslmes/mesXslRawMaterialCard/exportXls', + updatePriority = '/xslmes/mesXslRawMaterialCard/updatePriority', +} + +export const getExportUrl = Api.exportXls; +export const getImportUrl = Api.importExcel; + +export const list = (params) => defHttp.get({ url: Api.list, params }); + +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) => { + let url = isUpdate ? Api.edit : Api.save; + return defHttp.post({ url: url, params }); +}; + +export const updatePriority = (id: string, priorityPickup: string) => + defHttp.put({ url: Api.updatePriority, params: { id, priorityPickup } }, { joinParamsToUrl: true }); diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCard.data.ts b/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCard.data.ts new file mode 100644 index 0000000..2131077 --- /dev/null +++ b/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCard.data.ts @@ -0,0 +1,304 @@ +import { BasicColumn } from '/@/components/Table'; +import { FormSchema } from '/@/components/Table'; +import { render } from '/@/utils/common/renderUtils'; + +export const columns: BasicColumn[] = [ + { + title: '条码', + align: 'center', + dataIndex: 'barcode', + width: 160, + }, + { + title: '批次号', + align: 'center', + dataIndex: 'batchNo', + width: 160, + }, + { + title: '入场日期', + align: 'center', + dataIndex: 'entryDate', + width: 110, + customRender: ({ text }) => { + return !text ? '' : text.length > 10 ? text.substr(0, 10) : text; + }, + }, + { + title: '物料名称', + align: 'center', + dataIndex: 'materialName', + width: 160, + }, + { + title: '供应商名称', + align: 'center', + dataIndex: 'supplierName', + width: 150, + }, + { + title: '厂家物料名称', + align: 'center', + dataIndex: 'manufacturerMaterialName', + width: 150, + }, + { + title: '保质期', + align: 'center', + dataIndex: 'shelfLife', + width: 110, + }, + { + title: '总重', + align: 'center', + dataIndex: 'totalWeight', + width: 90, + }, + { + title: '剩余重量', + align: 'center', + dataIndex: 'remainingWeight', + width: 90, + }, + { + title: '剩余数量', + align: 'center', + dataIndex: 'remainingQuantity', + width: 90, + }, + { + title: '状态', + align: 'center', + dataIndex: 'status_dictText', + width: 80, + }, + { + title: '检测结果', + align: 'center', + dataIndex: 'testResult_dictText', + width: 90, + }, + { + title: '库区', + align: 'center', + dataIndex: 'warehouseArea', + width: 100, + }, + { + title: '卸货人', + align: 'center', + dataIndex: 'unloadOperator', + width: 90, + }, + { + title: '优先出库', + align: 'center', + dataIndex: 'priorityPickup', + width: 90, + customRender: ({ text }) => { + return render.renderSwitch(text, [{ text: '是', value: '1' }, { text: '否', value: '0' }]); + }, + }, + { + title: '创建时间', + align: 'center', + dataIndex: 'createTime', + width: 160, + }, +]; + +export const searchFormSchema: FormSchema[] = [ + { + label: '条码', + field: 'barcode', + component: 'JInput', + colProps: { span: 6 }, + }, + { + label: '批次号', + field: 'batchNo', + component: 'JInput', + colProps: { span: 6 }, + }, + { + label: '物料名称', + field: 'materialName', + component: 'JInput', + colProps: { span: 6 }, + }, + { + label: '供应商名称', + field: 'supplierName', + component: 'JInput', + colProps: { span: 6 }, + }, + { + label: '状态', + field: 'status', + component: 'JDictSelectTag', + componentProps: { dictCode: 'xslmes_card_status' }, + colProps: { span: 6 }, + }, + { + label: '检测结果', + field: 'testResult', + component: 'JDictSelectTag', + componentProps: { dictCode: 'xslmes_test_result' }, + colProps: { span: 6 }, + }, + { + label: '入场日期', + field: 'entryDate', + component: 'RangePicker', + componentProps: { showTime: false, valueFormat: 'YYYY-MM-DD' }, + colProps: { span: 8 }, + }, +]; + +export const formSchema: FormSchema[] = [ + { + label: '', + field: 'id', + component: 'Input', + show: false, + }, + { + label: '条码', + field: 'barcode', + component: 'Input', + componentProps: { placeholder: '请输入条码' }, + colProps: { span: 12 }, + }, + { + label: '批次号', + field: 'batchNo', + component: 'Input', + componentProps: { placeholder: '请输入批次号' }, + colProps: { span: 12 }, + }, + { + label: '入场日期', + field: 'entryDate', + component: 'DatePicker', + componentProps: { showTime: false, valueFormat: 'YYYY-MM-DD', placeholder: '请选择入场日期' }, + colProps: { span: 12 }, + }, + { + label: '物料ID', + field: 'materialId', + component: 'Input', + show: false, + }, + { + label: '物料名称', + field: 'materialName', + component: 'Input', + componentProps: { placeholder: '请输入物料名称' }, + colProps: { span: 12 }, + }, + { + label: '物料描述', + field: 'materialDesc', + component: 'InputTextArea', + componentProps: { placeholder: '请输入物料描述', rows: 2 }, + colProps: { span: 24 }, + }, + { + label: '供应商ID', + field: 'supplierId', + component: 'Input', + show: false, + }, + { + label: '供应商名称', + field: 'supplierName', + component: 'Input', + componentProps: { placeholder: '请输入供应商名称' }, + colProps: { span: 12 }, + }, + { + label: '厂家物料名称', + field: 'manufacturerMaterialName', + component: 'Input', + componentProps: { placeholder: '请输入厂家物料名称' }, + colProps: { span: 12 }, + }, + { + label: '保质期', + field: 'shelfLife', + component: 'Input', + componentProps: { placeholder: '请输入保质期' }, + colProps: { span: 12 }, + }, + { + label: '总重', + field: 'totalWeight', + component: 'InputNumber', + componentProps: { placeholder: '请输入总重', precision: 3 }, + colProps: { span: 12 }, + }, + { + label: '剩余重量', + field: 'remainingWeight', + component: 'InputNumber', + componentProps: { placeholder: '请输入剩余重量', precision: 3 }, + colProps: { span: 12 }, + }, + { + label: '剩余数量', + field: 'remainingQuantity', + component: 'InputNumber', + componentProps: { placeholder: '请输入剩余数量' }, + colProps: { span: 12 }, + }, + { + label: '状态', + field: 'status', + component: 'JDictSelectTag', + componentProps: { dictCode: 'xslmes_card_status', placeholder: '请选择状态' }, + colProps: { span: 12 }, + }, + { + label: '检测结果', + field: 'testResult', + component: 'JDictSelectTag', + componentProps: { dictCode: 'xslmes_test_result', placeholder: '请选择检测结果' }, + colProps: { span: 12 }, + }, + { + label: '库区', + field: 'warehouseArea', + component: 'Input', + componentProps: { placeholder: '请输入库区' }, + colProps: { span: 12 }, + }, + { + label: '卸货人', + field: 'unloadOperator', + component: 'Input', + componentProps: { placeholder: '请输入卸货人' }, + colProps: { span: 12 }, + }, + { + label: '优先出库', + field: 'priorityPickup', + component: 'JSwitch', + componentProps: { options: ['1', '0'] }, + colProps: { span: 12 }, + }, +]; + +export const superQuerySchema = { + barcode: { title: '条码', order: 0, view: 'text' }, + batchNo: { title: '批次号', order: 1, view: 'text' }, + entryDate: { title: '入场日期', order: 2, view: 'date' }, + materialName: { title: '物料名称', order: 3, view: 'text' }, + supplierName: { title: '供应商名称', order: 4, view: 'text' }, + totalWeight: { title: '总重', order: 5, view: 'number' }, + remainingWeight: { title: '剩余重量', order: 6, view: 'number' }, + remainingQuantity: { title: '剩余数量', order: 7, view: 'number' }, + status: { title: '状态', order: 8, view: 'list', dictCode: 'xslmes_card_status' }, + testResult: { title: '检测结果', order: 9, view: 'list', dictCode: 'xslmes_test_result' }, + warehouseArea: { title: '库区', order: 10, view: 'text' }, + createTime: { title: '创建时间', order: 11, view: 'datetime' }, +}; diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCardList.vue b/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCardList.vue new file mode 100644 index 0000000..024e6f9 --- /dev/null +++ b/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCardList.vue @@ -0,0 +1,172 @@ + + + + + diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/components/MesXslRawMaterialCardModal.vue b/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/components/MesXslRawMaterialCardModal.vue new file mode 100644 index 0000000..a80223d --- /dev/null +++ b/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/components/MesXslRawMaterialCardModal.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialEntry/MesXslRawMaterialEntry.data.ts b/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialEntry/MesXslRawMaterialEntry.data.ts index 3bf091f..2f1ad37 100644 --- a/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialEntry/MesXslRawMaterialEntry.data.ts +++ b/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialEntry/MesXslRawMaterialEntry.data.ts @@ -156,22 +156,23 @@ export const formSchema: FormSchema[] = [ componentProps: { min: 0, precision: 2, placeholder: '请输入总重', style: { width: '100%' } }, }, { + // 字段升级为字符串类型,支持桌面端拆码明细多行拼接(如 20/1/) label: '总份数', field: 'totalPortions', - component: 'InputNumber', - componentProps: { min: 0, placeholder: '请输入总份数', style: { width: '100%' } }, + component: 'Input', + componentProps: { placeholder: '请输入总份数(多行明细用 / 拼接)', style: { width: '100%' } }, }, { label: '每份总重(KG)', field: 'portionWeight', - component: 'InputNumber', - componentProps: { min: 0, precision: 2, placeholder: '请输入每份总重', style: { width: '100%' } }, + component: 'Input', + componentProps: { placeholder: '请输入每份总重(多行明细用 / 拼接)', style: { width: '100%' } }, }, { label: '每份包数', field: 'portionPackages', - component: 'InputNumber', - componentProps: { min: 0, placeholder: '请输入每份包数', style: { width: '100%' } }, + component: 'Input', + componentProps: { placeholder: '请输入每份包数(多行明细用 / 拼接)', style: { width: '100%' } }, }, { label: '检测结果', diff --git a/yy-admin-master/YY.Admin.Core/Core/Events/MesXslRawMaterialCardChangedEvent.cs b/yy-admin-master/YY.Admin.Core/Core/Events/MesXslRawMaterialCardChangedEvent.cs new file mode 100644 index 0000000..592db81 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Core/Events/MesXslRawMaterialCardChangedEvent.cs @@ -0,0 +1,13 @@ +using Prism.Events; + +namespace YY.Admin.Core.Events; + +public class RawMaterialCardChangedPayload +{ + public string Action { get; set; } = string.Empty; + public string? CardId { get; set; } +} + +public class RawMaterialCardChangedEvent : PubSubEvent +{ +} diff --git a/yy-admin-master/YY.Admin.Core/Core/Services/IRawMaterialCardService.cs b/yy-admin-master/YY.Admin.Core/Core/Services/IRawMaterialCardService.cs new file mode 100644 index 0000000..bd675f7 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Core/Services/IRawMaterialCardService.cs @@ -0,0 +1,18 @@ +using YY.Admin.Core.Entity; + +namespace YY.Admin.Core.Services; + +public record RawMaterialCardPageResult(List Records, long Total, int Current, int Size); + +public interface IRawMaterialCardService +{ + Task PageAsync(int pageNo, int pageSize, + string? barcode = null, string? batchNo = null, string? materialName = null, + string? supplierName = null, string? status = null, CancellationToken ct = default); + Task GetByIdAsync(string id, CancellationToken ct = default); + Task AddAsync(MesXslRawMaterialCard card, CancellationToken ct = default); + Task EditAsync(MesXslRawMaterialCard card, CancellationToken ct = default); + Task DeleteAsync(string id, CancellationToken ct = default); + Task DeleteBatchAsync(string ids, CancellationToken ct = default); + Task UpdatePriorityAsync(string id, string priorityPickup, CancellationToken ct = default); +} diff --git a/yy-admin-master/YY.Admin.Core/Entity/MesXslRawMaterialCard.cs b/yy-admin-master/YY.Admin.Core/Entity/MesXslRawMaterialCard.cs new file mode 100644 index 0000000..167aa9d --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/MesXslRawMaterialCard.cs @@ -0,0 +1,57 @@ +namespace YY.Admin.Core.Entity; + +public class MesXslRawMaterialCard +{ + public string? Id { get; set; } + public string? Barcode { get; set; } + public string? BatchNo { get; set; } + public DateTime? EntryDate { get; set; } + public string? MaterialId { get; set; } + public string? MaterialName { get; set; } + public string? MaterialDesc { get; set; } + public string? SupplierId { get; set; } + public string? SupplierName { get; set; } + public string? ManufacturerMaterialName { get; set; } + public string? ShelfLife { get; set; } + public decimal? TotalWeight { get; set; } + public decimal? RemainingWeight { get; set; } + public int? RemainingQuantity { get; set; } + + /// 状态:1正常 0异常(字典 xslmes_card_status) + public string? Status { get; set; } + + /// 检测结果:0未检 1合格 2不合格(字典 xslmes_test_result) + public string? TestResult { get; set; } + + public string? WarehouseArea { get; set; } + public string? UnloadOperator { get; set; } + + /// 优先出库:1是 0否 + public string? PriorityPickup { get; set; } + + public string? CreateBy { get; set; } + public DateTime? CreateTime { get; set; } + public string? UpdateBy { get; set; } + public DateTime? UpdateTime { get; set; } + public int? TenantId { get; set; } + + public string StatusText => Status switch + { + "1" => "正常", + "0" => "异常", + _ => Status ?? "" + }; + + public string TestResultText => TestResult switch + { + "0" => "未检", + "1" => "合格", + "2" => "不合格", + _ => TestResult ?? "" + }; + + public bool PriorityPickupBool => PriorityPickup == "1"; + public string PriorityPickupText => PriorityPickup == "1" ? "是" : "否"; + + public string EntryDateText => EntryDate.HasValue ? EntryDate.Value.ToString("yyyy-MM-dd") : ""; +} diff --git a/yy-admin-master/YY.Admin.Core/Entity/MesXslRawMaterialEntry.cs b/yy-admin-master/YY.Admin.Core/Entity/MesXslRawMaterialEntry.cs index 506d21c..2b4d7e5 100644 --- a/yy-admin-master/YY.Admin.Core/Entity/MesXslRawMaterialEntry.cs +++ b/yy-admin-master/YY.Admin.Core/Entity/MesXslRawMaterialEntry.cs @@ -17,9 +17,12 @@ public class MesXslRawMaterialEntry public string? ManufacturerMaterialName { get; set; } public string? ShelfLife { get; set; } public double? TotalWeight { get; set; } - public int? TotalPortions { get; set; } - public double? PortionWeight { get; set; } - public int? PortionPackages { get; set; } + + // 总份数 / 每份总重 / 每份包数:与后端同步升级为字符串, + // 用于持久化「拆码明细」多行拼接(如 20/1/、100/200/)。 + public string? TotalPortions { get; set; } + public string? PortionWeight { get; set; } + public string? PortionPackages { get; set; } /// 检测结果:0未检 1合格 2不合格 public string? TestResult { get; set; } diff --git a/yy-admin-master/YY.Admin.Core/SeedData/SysMenuSeedData.cs b/yy-admin-master/YY.Admin.Core/SeedData/SysMenuSeedData.cs index c12c10a..8cd4a22 100644 --- a/yy-admin-master/YY.Admin.Core/SeedData/SysMenuSeedData.cs +++ b/yy-admin-master/YY.Admin.Core/SeedData/SysMenuSeedData.cs @@ -42,6 +42,8 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData new SysMenu{ Id=1300150010701, Pid=1300150000101, Title="原料入场记录", Path="/xslmes/mesXslRawMaterialEntry", Name="mesXslRawMaterialEntry", Component="RawMaterialEntryListView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=106 }, // 新增原料入场记录(独立页面) 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 }, #endregion diff --git a/yy-admin-master/YY.Admin.Core/SeedData/SysTenantMenuSeedData.cs b/yy-admin-master/YY.Admin.Core/SeedData/SysTenantMenuSeedData.cs index b0e0ebd..2b15009 100644 --- a/yy-admin-master/YY.Admin.Core/SeedData/SysTenantMenuSeedData.cs +++ b/yy-admin-master/YY.Admin.Core/SeedData/SysTenantMenuSeedData.cs @@ -215,6 +215,7 @@ public class SysTenantMenuSeedData : ISqlSugarEntitySeedData new SysTenantMenu(){ TenantId=1300000000001,MenuId= 1300300040501 }, new SysTenantMenu(){ TenantId=1300000000001,MenuId = 1300200010201}, new SysTenantMenu() { TenantId = 1300000000001,MenuId = 1300600040101}, + new SysTenantMenu(){ TenantId=1300000000001, MenuId=1300150010901 }, }; } diff --git a/yy-admin-master/YY.Admin.Services/Service/RawMaterialCard/RawMaterialCardService.cs b/yy-admin-master/YY.Admin.Services/Service/RawMaterialCard/RawMaterialCardService.cs new file mode 100644 index 0000000..1ca8652 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/RawMaterialCard/RawMaterialCardService.cs @@ -0,0 +1,735 @@ +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.RawMaterialCard; + +public class RawMaterialCardService : IRawMaterialCardService, 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 _pendingOps = new(); + private List _localCache = new(); + + private static readonly JsonSerializerOptions _jsonOpts = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new NullableDateTimeJsonConverter() } + }; + + public RawMaterialCardService( + 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, "mes-xsl-raw-material-card-pending-ops.json"); + _cacheFilePath = Path.Combine(appDataDir, "mes-xsl-raw-material-card-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("JeecgIntegration:BaseUrl") ?? "http://localhost:8080/jeecg-boot").TrimEnd('/'); + private int DefaultTenantId => (int?)_configuration.GetValue("JeecgIntegration:DefaultTenantId") ?? 1002; + + private HttpClient CreateClient() => _httpClientFactory.CreateClient("JeecgApi"); + + public async Task PageAsync(int pageNo, int pageSize, + string? barcode = null, string? batchNo = null, string? materialName = null, + string? supplierName = null, string? status = null, CancellationToken ct = default) + { + List? 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, barcode, batchNo, materialName, supplierName, status); + var total = filtered.Count; + var records = filtered.Skip(Math.Max(0, (pageNo - 1) * pageSize)).Take(pageSize).ToList(); + return new RawMaterialCardPageResult(records, total, pageNo, pageSize); + } + + public async Task GetByIdAsync(string id, CancellationToken ct = default) + { + if (_networkMonitor.IsOnline) + { + try + { + var url = $"{BaseUrl}/xslmes/mesXslRawMaterialCard/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(_jsonOpts); + } + catch (Exception ex) + { + _logger.Warning($"[原材料卡片] 远端查询异常,回退缓存 id={id}: {ex.Message}"); + } + } + + lock (_cacheLock) + { + return _localCache.FirstOrDefault(c => string.Equals(c.Id, id, StringComparison.OrdinalIgnoreCase)) is { } found + ? Clone(found) : null; + } + } + + public async Task AddAsync(MesXslRawMaterialCard card, CancellationToken ct = default) + { + if (!card.TenantId.HasValue || card.TenantId.Value <= 0) + card.TenantId = DefaultTenantId; + + var local = Clone(card); + 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($"[原材料卡片] 远端新增异常,转离线入队 id={local.Id}: {ex.Message}"); + } + } + + EnqueuePendingOperation(new RawMaterialCardPendingOperation + { + OpType = RawMaterialCardOperationType.Add, + CardId = local.Id, + Card = local, + CreatedAt = DateTime.UtcNow + }); + UpsertLocalCache(local); + return true; + } + + public async Task EditAsync(MesXslRawMaterialCard card, CancellationToken ct = default) + { + if (!card.TenantId.HasValue || card.TenantId.Value <= 0) + card.TenantId = DefaultTenantId; + var local = Clone(card); + + 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($"[原材料卡片] 远端修改异常,转离线入队 id={local.Id}: {ex.Message}"); + } + } + + EnqueuePendingOperation(new RawMaterialCardPendingOperation + { + OpType = RawMaterialCardOperationType.Edit, + CardId = local.Id, + Card = local, + AnchorUpdateTime = local.UpdateTime, + CreatedAt = DateTime.UtcNow + }); + UpsertLocalCache(local); + return true; + } + + public async Task DeleteAsync(string id, CancellationToken ct = default) + { + 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($"[原材料卡片] 远端删除异常,转离线入队 id={id}: {ex.Message}"); + } + } + + DateTime? anchor; + lock (_cacheLock) + { + anchor = _localCache.FirstOrDefault(c => string.Equals(c.Id, id, StringComparison.OrdinalIgnoreCase))?.UpdateTime; + } + EnqueuePendingOperation(new RawMaterialCardPendingOperation + { + OpType = RawMaterialCardOperationType.Delete, + CardId = id, + AnchorUpdateTime = anchor, + CreatedAt = DateTime.UtcNow + }); + RemoveFromLocalCache(id); + return true; + } + + public async Task DeleteBatchAsync(string ids, CancellationToken ct = default) + { + var idList = ids.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var allSuccess = true; + foreach (var id in idList) + allSuccess &= await DeleteAsync(id, ct).ConfigureAwait(false); + return allSuccess; + } + + public async Task UpdatePriorityAsync(string id, string priorityPickup, CancellationToken ct = default) + { + if (_networkMonitor.IsOnline) + { + try + { + var url = $"{BaseUrl}/xslmes/mesXslRawMaterialCard/updatePriority?id={Uri.EscapeDataString(id)}&priorityPickup={Uri.EscapeDataString(priorityPickup)}&tenantId={DefaultTenantId}"; + using var client = CreateClient(); + var resp = await client.PutAsync(url, null, ct).ConfigureAwait(false); + var ok = resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false); + if (ok) + { + UpdateLocalPriority(id, priorityPickup); + return true; + } + return false; + } + catch (Exception ex) + { + _logger.Warning($"[原材料卡片] 远端优先出库更新异常 id={id}: {ex.Message}"); + } + } + + UpdateLocalPriority(id, priorityPickup); + return true; + } + + // ─────────────────────────── Remote helpers ──────────────────────────── + + private async Task> FetchRemoteListAsync(CancellationToken ct) + { + var query = HttpUtility.ParseQueryString(string.Empty); + query["pageNo"] = "1"; + query["pageSize"] = "10000"; + query["tenantId"] = DefaultTenantId.ToString(); + var url = $"{BaseUrl}/xslmes/mesXslRawMaterialCard/anon/list?{query}"; + using var client = CreateClient(); + 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>(_jsonOpts) ?? new(); + } + + private async Task FetchRemoteSingleAsync(string id, CancellationToken ct) + { + try + { + var url = $"{BaseUrl}/xslmes/mesXslRawMaterialCard/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 resultEl.Deserialize(_jsonOpts); + return null; + } + catch { return null; } + } + + private async Task RemoteAddAsync(MesXslRawMaterialCard card, CancellationToken ct) + { + var url = $"{BaseUrl}/xslmes/mesXslRawMaterialCard/anon/add?tenantId={DefaultTenantId}"; + var payload = Clone(card); + if (IsLocalTempId(payload.Id)) payload.Id = null; + return await PostJsonAsync(url, payload, ct).ConfigureAwait(false); + } + + private async Task<(bool Ok, bool IsVersionConflict)> RemoteEditAsync(MesXslRawMaterialCard card, CancellationToken ct) + { + var url = $"{BaseUrl}/xslmes/mesXslRawMaterialCard/anon/edit?tenantId={DefaultTenantId}"; + return await PostJsonCheckVersionAsync(url, card, ct).ConfigureAwait(false); + } + + private async Task RemoteDeleteAsync(string id, CancellationToken ct) + { + var url = $"{BaseUrl}/xslmes/mesXslRawMaterialCard/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 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); + return resp.IsSuccessStatusCode && 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)) + { + var msg = msgEl.GetString() ?? ""; + if (msg.Contains("已被他人修改")) return (false, true); + } + return (false, false); + } + catch { return (true, false); } + } + + private static async Task 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; } + } + + // ─────────────────────────── Reconnect sync ──────────────────────────── + + 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().Publish(new RawMaterialCardChangedPayload { 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().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 PushPendingOnReconnectAsync(CancellationToken ct) + { + if (!await _syncLock.WaitAsync(0, ct).ConfigureAwait(false)) + return new PushPendingResult(0, 0, 0); + try + { + List 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)) + RemovePendingOpsByCardId(result.EntityId!); + continue; + } + + lock (_cacheLock) + { + if (op.OpType == RawMaterialCardOperationType.Add) newPushed++; + else pushed++; + _pendingOps.RemoveAll(x => x.Id == op.Id); + SavePendingOpsToDiskUnsafe(); + } + } + + return new PushPendingResult(pushed, conflicts, newPushed); + } + finally { _syncLock.Release(); } + } + + private async Task ExecutePendingOperationAsync(RawMaterialCardPendingOperation op, CancellationToken ct) + { + try + { + switch (op.OpType) + { + case RawMaterialCardOperationType.Add: + { + var ok = op.Card != null && await RemoteAddAsync(op.Card, ct).ConfigureAwait(false); + return ok ? new(true, false, op.CardId) : new(false, false, null); + } + case RawMaterialCardOperationType.Edit: + { + if (op.Card == null || string.IsNullOrWhiteSpace(op.Card.Id)) return new(false, false, null); + var id = op.Card.Id; + var remote = await FetchRemoteSingleAsync(id, ct).ConfigureAwait(false); + if (remote != null && op.AnchorUpdateTime != null && remote.UpdateTime != op.AnchorUpdateTime) + { + UpsertLocalCache(remote); + return new(true, true, id); + } + var (ok, isConflict) = await RemoteEditAsync(op.Card, ct).ConfigureAwait(false); + if (isConflict) + { + var fresh = await FetchRemoteSingleAsync(id, ct).ConfigureAwait(false); + if (fresh != null) UpsertLocalCache(fresh); + return new(true, true, id); + } + return ok ? new(true, false, id) : new(false, false, null); + } + case RawMaterialCardOperationType.Delete: + { + if (string.IsNullOrWhiteSpace(op.CardId)) return new(false, false, null); + var id = op.CardId!; + var remote = await FetchRemoteSingleAsync(id, ct).ConfigureAwait(false); + if (remote == null) return new(true, false, id); + if (op.AnchorUpdateTime != null && remote.UpdateTime != op.AnchorUpdateTime) + { + UpsertLocalCache(remote); + return new(true, true, id); + } + var ok = await RemoteDeleteAsync(id, ct).ConfigureAwait(false); + return ok ? new(true, false, id) : new(false, false, null); + } + default: + return new(true, false, null); + } + } + catch (Exception ex) + { + _logger.Warning($"[原材料卡片] 执行pending异常 op={op.OpType}: {ex.Message}"); + return new(false, false, null); + } + } + + // ─────────────────────────── Local cache helpers ──────────────────────────── + + private void RemovePendingOpsByCardId(string cardId) + { + lock (_cacheLock) + { + _pendingOps.RemoveAll(x => + (!string.IsNullOrWhiteSpace(x.CardId) && string.Equals(x.CardId, cardId, StringComparison.OrdinalIgnoreCase)) || + (x.Card?.Id != null && string.Equals(x.Card.Id, cardId, StringComparison.OrdinalIgnoreCase))); + SavePendingOpsToDiskUnsafe(); + } + } + + private void EnqueuePendingOperation(RawMaterialCardPendingOperation op) + { + lock (_cacheLock) { _pendingOps.Add(op); SavePendingOpsToDiskUnsafe(); } + } + + private void UpsertLocalCache(MesXslRawMaterialCard card) + { + lock (_cacheLock) + { + var idx = _localCache.FindIndex(c => string.Equals(c.Id, card.Id, StringComparison.OrdinalIgnoreCase)); + if (idx >= 0) _localCache[idx] = Clone(card); + else _localCache.Insert(0, Clone(card)); + SaveCacheToDiskUnsafe(); + } + } + + private void RemoveFromLocalCache(string id) + { + lock (_cacheLock) + { + _localCache.RemoveAll(c => string.Equals(c.Id, id, StringComparison.OrdinalIgnoreCase)); + SaveCacheToDiskUnsafe(); + } + } + + private void UpdateLocalPriority(string id, string priorityPickup) + { + lock (_cacheLock) + { + var item = _localCache.FirstOrDefault(c => string.Equals(c.Id, id, StringComparison.OrdinalIgnoreCase)); + if (item != null) { item.PriorityPickup = priorityPickup; SaveCacheToDiskUnsafe(); } + } + } + + private List ApplyPendingOpsSnapshotUnsafe(List source) + { + var map = source.Where(c => !string.IsNullOrWhiteSpace(c.Id)) + .ToDictionary(c => c.Id!, Clone, StringComparer.OrdinalIgnoreCase); + + foreach (var op in _pendingOps.OrderBy(x => x.CreatedAt)) + { + switch (op.OpType) + { + case RawMaterialCardOperationType.Add: + case RawMaterialCardOperationType.Edit: + if (op.Card != null && !string.IsNullOrWhiteSpace(op.Card.Id)) + map[op.Card.Id] = Clone(op.Card); + break; + case RawMaterialCardOperationType.Delete: + if (!string.IsNullOrWhiteSpace(op.CardId)) map.Remove(op.CardId); + break; + } + } + return map.Values.ToList(); + } + + private static List ApplyFilters(List source, + string? barcode, string? batchNo, string? materialName, string? supplierName, string? status) + { + IEnumerable q = source; + if (!string.IsNullOrWhiteSpace(barcode)) + q = q.Where(c => (c.Barcode ?? "").Contains(barcode, StringComparison.OrdinalIgnoreCase)); + if (!string.IsNullOrWhiteSpace(batchNo)) + q = q.Where(c => (c.BatchNo ?? "").Contains(batchNo, StringComparison.OrdinalIgnoreCase)); + if (!string.IsNullOrWhiteSpace(materialName)) + q = q.Where(c => (c.MaterialName ?? "").Contains(materialName, StringComparison.OrdinalIgnoreCase)); + if (!string.IsNullOrWhiteSpace(supplierName)) + q = q.Where(c => (c.SupplierName ?? "").Contains(supplierName, StringComparison.OrdinalIgnoreCase)); + if (!string.IsNullOrWhiteSpace(status)) + q = q.Where(c => string.Equals(c.Status, status, StringComparison.OrdinalIgnoreCase)); + return q.OrderByDescending(c => c.CreateTime ?? DateTime.MinValue).ToList(); + } + + private void LoadPendingOpsFromDisk() + { + try + { + if (!File.Exists(_pendingOpsFilePath)) return; + var data = JsonSerializer.Deserialize>(File.ReadAllText(_pendingOpsFilePath), _jsonOpts); + _pendingOps = data ?? new(); + } + catch (Exception ex) { _pendingOps = new(); _logger.Warning($"[原材料卡片] 载入待上传失败:{ex.Message}"); } + } + + private void LoadCacheFromDisk() + { + try + { + if (!File.Exists(_cacheFilePath)) return; + var data = JsonSerializer.Deserialize>(File.ReadAllText(_cacheFilePath), _jsonOpts); + _localCache = data ?? new(); + } + catch (Exception ex) { _localCache = new(); _logger.Warning($"[原材料卡片] 载入缓存失败:{ex.Message}"); } + } + + private void SavePendingOpsToDiskUnsafe() => + File.WriteAllText(_pendingOpsFilePath, JsonSerializer.Serialize(_pendingOps, _jsonOpts)); + + private void SaveCacheToDiskUnsafe() => + File.WriteAllText(_cacheFilePath, JsonSerializer.Serialize(_localCache, _jsonOpts)); + + private static MesXslRawMaterialCard Clone(MesXslRawMaterialCard input) => new() + { + Id = input.Id, + Barcode = input.Barcode, + BatchNo = input.BatchNo, + EntryDate = input.EntryDate, + MaterialId = input.MaterialId, + MaterialName = input.MaterialName, + MaterialDesc = input.MaterialDesc, + SupplierId = input.SupplierId, + SupplierName = input.SupplierName, + ManufacturerMaterialName = input.ManufacturerMaterialName, + ShelfLife = input.ShelfLife, + TotalWeight = input.TotalWeight, + RemainingWeight = input.RemainingWeight, + RemainingQuantity = input.RemainingQuantity, + Status = input.Status, + TestResult = input.TestResult, + WarehouseArea = input.WarehouseArea, + UnloadOperator = input.UnloadOperator, + PriorityPickup = input.PriorityPickup, + CreateBy = input.CreateBy, + CreateTime = input.CreateTime, + UpdateBy = input.UpdateBy, + UpdateTime = input.UpdateTime, + TenantId = input.TenantId + }; + + private static bool IsLocalTempId(string? id) => + !string.IsNullOrWhiteSpace(id) && id.StartsWith("local-", StringComparison.OrdinalIgnoreCase); + + private sealed class RawMaterialCardPendingOperation + { + public string Id { get; set; } = Guid.NewGuid().ToString("N"); + public RawMaterialCardOperationType OpType { get; set; } + public string? CardId { get; set; } + public MesXslRawMaterialCard? Card { get; set; } + public DateTime? AnchorUpdateTime { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public int RetryCount { get; set; } = 0; + } + + private enum RawMaterialCardOperationType { Add = 1, Edit = 2, Delete = 3 } + + private sealed class NullableDateTimeJsonConverter : JsonConverter + { + 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($"无法将 JSON 值转换为 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 HH:mm:ss")); return; } + writer.WriteNullValue(); + } + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/RawMaterialCard/RawMaterialCardSyncCoordinator.cs b/yy-admin-master/YY.Admin.Services/Service/RawMaterialCard/RawMaterialCardSyncCoordinator.cs new file mode 100644 index 0000000..9fa1cb6 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/RawMaterialCard/RawMaterialCardSyncCoordinator.cs @@ -0,0 +1,85 @@ +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.RawMaterialCard; + +/// +/// 监听 STOMP 收到的原材料卡片变更信号,转发为 Prism 事件,触发列表刷新。 +/// +public class RawMaterialCardSyncCoordinator : ISingletonDependency +{ + private readonly IEventAggregator _eventAggregator; + private readonly ILoggerService _logger; + private SubscriptionToken? _remoteCommandToken; + private SubscriptionToken? _networkStatusToken; + + public RawMaterialCardSyncCoordinator( + IEventAggregator eventAggregator, + SyncPollManager pollManager, + ILoggerService logger) + { + _eventAggregator = eventAggregator; + _logger = logger; + + _remoteCommandToken = _eventAggregator + .GetEvent() + .Subscribe(OnRemoteCommand, ThreadOption.BackgroundThread); + _networkStatusToken = _eventAggregator + .GetEvent() + .Subscribe(OnNetworkStatusChanged, ThreadOption.BackgroundThread); + + pollManager.Register("原材料卡片", () => + { + _eventAggregator.GetEvent() + .Publish(new RawMaterialCardChangedPayload { Action = "poll" }); + return Task.CompletedTask; + }); + + _logger.Information("[原材料卡片推送] RawMaterialCardSyncCoordinator 已启动"); + } + + private void OnNetworkStatusChanged(NetworkStatusChangedPayload payload) + { + if (!payload.IsOnline) return; + _logger.Information("[原材料卡片推送] 网络恢复,触发补偿刷新"); + _eventAggregator.GetEvent() + .Publish(new RawMaterialCardChangedPayload { 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("RAW_MATERIAL_CARD_CHANGED", StringComparison.OrdinalIgnoreCase)) + { + _logger.Information($"[原材料卡片推送] 非原材料卡片命令 cmd={cmd},忽略"); + return; + } + + doc.RootElement.TryGetProperty("action", out var actionEl); + doc.RootElement.TryGetProperty("cardId", out var idEl); + + var changedPayload = new RawMaterialCardChangedPayload + { + Action = actionEl.GetString() ?? string.Empty, + CardId = idEl.ValueKind == JsonValueKind.String ? idEl.GetString() : null + }; + + _logger.Information($"[原材料卡片推送] 收到变更信号: action={changedPayload.Action}, cardId={changedPayload.CardId}"); + _eventAggregator.GetEvent().Publish(changedPayload); + } + catch (Exception ex) + { + _logger.Warning($"[原材料卡片推送] 处理 STOMP 信号失败: {ex.Message}"); + } + } +} diff --git a/yy-admin-master/YY.Admin/Controls/DateTimeListPicker.xaml b/yy-admin-master/YY.Admin/Controls/DateTimeListPicker.xaml new file mode 100644 index 0000000..4148eb1 --- /dev/null +++ b/yy-admin-master/YY.Admin/Controls/DateTimeListPicker.xaml @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - -