From c3f8190537265197d58acbfcd5d930ea1dd5d4fd Mon Sep 17 00:00:00 2001 From: geht <2947093423@qq.com> Date: Wed, 13 May 2026 15:49:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=89=93=E5=8D=B0=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E7=BB=91=E5=AE=9A=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E4=B8=9A=E5=8A=A1=E4=B8=8E=E6=89=93=E5=8D=B0=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E7=9A=84=E6=98=A0=E5=B0=84=E9=85=8D=E7=BD=AE=E3=80=82?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=89=93=E5=8D=B0=E6=A8=A1=E6=9D=BF=E7=9A=84?= =?UTF-8?q?=E5=A2=9E=E5=88=A0=E6=94=B9=E6=9F=A5=E6=93=8D=E4=BD=9C=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=89=93=E5=8D=B0=E6=95=B0=E6=8D=AE=E7=9A=84?= =?UTF-8?q?=E7=94=9F=E6=88=90=E9=80=BB=E8=BE=91=EF=BC=8C=E6=8F=90=E5=8D=87?= =?UTF-8?q?=E6=89=93=E5=8D=B0=E6=A8=A1=E6=9D=BF=E7=9A=84=E7=81=B5=E6=B4=BB?= =?UTF-8?q?=E6=80=A7=E5=92=8C=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C=E3=80=82?= =?UTF-8?q?=E5=90=8C=E6=97=B6=EF=BC=8C=E6=96=B0=E5=A2=9E=E6=89=93=E5=8D=B0?= =?UTF-8?q?=E6=9C=BA=E6=9F=A5=E8=AF=A2=E6=8E=A5=E5=8F=A3=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E6=89=93=E5=8D=B0=E6=9C=8D=E5=8A=A1=E7=9A=84=E5=8F=AF?= =?UTF-8?q?=E7=94=A8=E6=80=A7=E5=92=8C=E5=AE=9E=E6=97=B6=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jeecg-module-xslmes/pom.xml | 6 + .../MesXslDesktopAnonController.java | 42 ++ .../MesXslRawMaterialCardController.java | 150 ++++++- .../print/catalog/PrintBizTypeCatalog.java | 73 +++ .../PrintBizTemplateBindController.java | 216 +++++++++ .../controller/PrintTemplateController.java | 211 +-------- .../print/entity/PrintBizTemplateBind.java | 47 ++ .../mapper/PrintBizTemplateBindMapper.java | 7 + .../service/IPrintBizTemplateBindService.java | 10 + .../impl/PrintBizTemplateBindServiceImpl.java | 24 + .../PrintServerEnvironmentService.java | 69 +++ .../support/PrintServerPdfJobService.java | 170 +++++++ .../print/util/PrintBizDataMappingUtil.java | 129 ++++++ .../PrintNativeTemplateFieldExtractor.java | 141 ++++++ .../modules/print/vo/PrintBizFieldItemVO.java | 22 + .../modules/print/vo/PrintBizTypeVO.java | 22 + .../print/vo/PrintTemplateFieldItemVO.java | 22 + .../V3.9.2_50__print_biz_template_bind.sql | 43 ++ .../bizTemplateBind/bizTemplateBind.api.ts | 29 ++ .../bizTemplateBind/bizTemplateBind.data.ts | 8 + .../src/views/print/bizTemplateBind/index.vue | 414 ++++++++++++++++++ .../print/template/utils/printDotBridge.ts | 14 + .../template/utils/printHtmlToPdfBase64.ts | 16 +- .../template/utils/printNativeViaPrintDot.ts | 3 +- .../MesXslRawMaterialCard.api.ts | 16 + .../MesXslRawMaterialCardList.vue | 317 +++++++++++++- .../RawMaterialCardPrintPreviewModal.vue | 211 +++++++++ .../Core/Services/IRawMaterialCardService.cs | 6 + .../RawMaterialCard/RawMaterialCardService.cs | 33 ++ .../RawMaterialCardListViewModel.cs | 67 ++- .../Views/Print/PrintPreviewWindow.xaml.cs | 7 +- .../RawMaterialCardListView.xaml | 7 +- 32 files changed, 2323 insertions(+), 229 deletions(-) create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/catalog/PrintBizTypeCatalog.java create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/controller/PrintBizTemplateBindController.java create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/entity/PrintBizTemplateBind.java create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/mapper/PrintBizTemplateBindMapper.java create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/service/IPrintBizTemplateBindService.java create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/service/impl/PrintBizTemplateBindServiceImpl.java create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/support/PrintServerEnvironmentService.java create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/support/PrintServerPdfJobService.java create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintBizDataMappingUtil.java create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintNativeTemplateFieldExtractor.java create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizFieldItemVO.java create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizTypeVO.java create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintTemplateFieldItemVO.java create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_50__print_biz_template_bind.sql create mode 100644 jeecgboot-vue3/src/views/print/bizTemplateBind/bizTemplateBind.api.ts create mode 100644 jeecgboot-vue3/src/views/print/bizTemplateBind/bizTemplateBind.data.ts create mode 100644 jeecgboot-vue3/src/views/print/bizTemplateBind/index.vue create mode 100644 jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/components/RawMaterialCardPrintPreviewModal.vue diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/pom.xml b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/pom.xml index 19b8ebd..23cc3e1 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/pom.xml +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/pom.xml @@ -18,5 +18,11 @@ org.jeecgframework.boot3 jeecg-boot-base-core + + + org.jeecgframework.boot3 + jeecg-system-biz + ${jeecgboot.version} + 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 c4f5c7e..d0b0813 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 @@ -9,9 +9,18 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.jeecg.common.api.vo.Result; import org.jeecg.common.system.query.QueryGenerator; import org.jeecg.common.util.oConvertUtils; +import org.jeecg.modules.print.entity.PrintBizTemplateBind; +import org.jeecg.modules.print.entity.PrintTemplate; +import org.jeecg.modules.print.service.IPrintBizTemplateBindService; +import org.jeecg.modules.print.service.IPrintTemplateService; +import org.jeecg.modules.print.util.PrintBizDataMappingUtil; import org.jeecg.modules.xslmes.constant.MesXslCustomerBizStatus; import org.jeecg.modules.xslmes.entity.MesXslCustomer; import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard; @@ -33,6 +42,8 @@ import org.jeecg.modules.xslmes.service.MesXslStompNotifyService; import org.springframework.web.bind.annotation.*; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import org.apache.commons.lang3.StringUtils; @@ -59,6 +70,9 @@ public class MesXslDesktopAnonController { private final IMesXslWarehouseService warehouseService; private final IMesXslWarehouseAreaService warehouseAreaService; private final MesXslStompNotifyService stompNotify; + private final IPrintBizTemplateBindService printBizTemplateBindService; + private final IPrintTemplateService printTemplateService; + private final ObjectMapper objectMapper; // ═══════════════════════════ 车辆管理 ═══════════════════════════ @@ -606,6 +620,34 @@ public class MesXslDesktopAnonController { return Result.OK((int) count); } + /** 与 PrintBizTypeCatalog / 业务打印绑定一致 */ + private static final String RAW_MATERIAL_CARD_BIZ_CODE = "MES_RAW_MATERIAL_CARD"; + + @Operation(summary = "原材料卡片-免密准备原生打印数据(桌面端用)") + @GetMapping("/xslmes/mesXslRawMaterialCard/anon/prepareNativePrint") + public Result> rawMaterialCardAnonPrepareNativePrint(@RequestParam(name = "id") String id) { + try { + MesXslRawMaterialCard card = rawMaterialCardService.getById(id); + if (card == null) return Result.error("未找到原材料卡片"); + PrintBizTemplateBind bind = printBizTemplateBindService.getByBizCode(RAW_MATERIAL_CARD_BIZ_CODE); + if (bind == null) return Result.error("请先在「业务打印绑定」中配置原材料卡片与打印模板"); + PrintTemplate tpl = printTemplateService.getById(bind.getTemplateId()); + if (tpl == null) return Result.error("绑定的打印模板不存在"); + ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson()); + JsonNode bizRoot = objectMapper.valueToTree(card); + ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping); + Map out = new HashMap<>(8); + out.put("cardId", card.getId()); + out.put("templateCode", bind.getTemplateCode()); + out.put("templateJson", tpl.getTemplateJson()); + out.put("printData", objectMapper.convertValue(printData, Map.class)); + return Result.OK(out); + } catch (Exception e) { + log.error("原材料卡片-免密准备打印数据失败 id={}", id, e); + return Result.error("准备打印数据失败: " + e.getMessage()); + } + } + // ═══════════════════════════ 仓库管理(只读,供桌面端下拉选取) ═══════════════════════════ @Operation(summary = "仓库-免密分页列表查询(供桌面端筛选使用)") 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 index 144d131..3ca108d 100644 --- 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 @@ -1,27 +1,48 @@ 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 com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.v3.oas.annotations.Operation; -import org.jeecg.common.aspect.annotation.AutoLog; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.shiro.authz.annotation.Logical; 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.modules.print.entity.PrintBizTemplateBind; +import org.jeecg.modules.print.entity.PrintTemplate; +import org.jeecg.modules.print.service.IPrintBizTemplateBindService; +import org.jeecg.modules.print.service.IPrintTemplateService; +import org.jeecg.modules.print.support.PrintServerEnvironmentService; +import org.jeecg.modules.print.support.PrintServerPdfJobService; +import org.jeecg.modules.print.util.PrintBizDataMappingUtil; +import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard; +import org.jeecg.modules.xslmes.service.IMesXslRawMaterialCardService; +import org.jeecg.modules.xslmes.service.MesXslStompNotifyService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.ModelAndView; /** * @Description: 原材料卡片 @@ -34,10 +55,24 @@ import org.apache.shiro.authz.annotation.RequiresPermissions; @RequestMapping("/xslmes/mesXslRawMaterialCard") @Slf4j public class MesXslRawMaterialCardController extends JeecgController { + + /** 与 PrintBizTypeCatalog / 业务打印绑定一致 */ + private static final String RAW_MATERIAL_CARD_BIZ_CODE = "MES_RAW_MATERIAL_CARD"; + @Autowired private IMesXslRawMaterialCardService mesXslRawMaterialCardService; @Autowired private MesXslStompNotifyService stompNotify; + @Autowired + private PrintServerEnvironmentService printServerEnvironmentService; + @Autowired + private PrintServerPdfJobService printServerPdfJobService; + @Autowired + private IPrintBizTemplateBindService printBizTemplateBindService; + @Autowired + private IPrintTemplateService printTemplateService; + @Autowired + private ObjectMapper objectMapper; /** * 分页列表查询 @@ -122,6 +157,87 @@ public class MesXslRawMaterialCardController extends JeecgController> queryPrinters() { + return Result.OK(printServerEnvironmentService.buildPrinterQueryResult()); + } + + /** + * 根据业务打印绑定生成模板 JSON + 映射后的 printData,供前端生成 PDF 后调用 printPdf + */ + @Operation(summary = "原材料卡片-准备原生打印数据") + @GetMapping(value = "/prepareNativePrint") + /** 与 printPdf 一致:准备模板与打印数据属于「打印」动作,使用 edit 权限 */ + @RequiresPermissions("xslmes:mes_xsl_raw_material_card:edit") + public Result> prepareNativePrint(@RequestParam(name = "id") String id) { + try { + MesXslRawMaterialCard card = mesXslRawMaterialCardService.getById(id); + if (card == null) { + return Result.error("未找到原材料卡片"); + } + PrintBizTemplateBind bind = + printBizTemplateBindService.getByBizCode(RAW_MATERIAL_CARD_BIZ_CODE); + if (bind == null) { + return Result.error("请先在「业务打印绑定」中配置原材料卡片与打印模板"); + } + PrintTemplate tpl = printTemplateService.getById(bind.getTemplateId()); + if (tpl == null) { + return Result.error("绑定的打印模板不存在"); + } + ArrayNode mapping = + PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson()); + JsonNode bizRoot = objectMapper.valueToTree(card); + ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping); + Map out = new HashMap<>(8); + out.put("cardId", card.getId()); + out.put("templateCode", bind.getTemplateCode()); + out.put("templateJson", tpl.getTemplateJson()); + out.put("paperWidthMm", tpl.getPaperWidthMm()); + out.put("paperHeightMm", tpl.getPaperHeightMm()); + out.put("paperOrientation", tpl.getPaperOrientation()); + out.put("printData", objectMapper.convertValue(printData, Map.class)); + return Result.OK(out); + } catch (Exception e) { + log.error("原材料卡片准备打印数据失败 id={}", id, e); + return Result.error("准备打印数据失败: " + e.getMessage()); + } + } + + /** + * 将前端生成的 PDF Base64 提交到服务器打印机(与 /print/template/directPrintPdf 一致) + */ + @AutoLog(value = "原材料卡片-PDF后端打印") + @Operation(summary = "原材料卡片-PDF后端打印") + @PostMapping(value = "/printPdf") + @RequiresPermissions("xslmes:mes_xsl_raw_material_card:edit") + public Result printPdf(@RequestBody Map body) { + String id = String.valueOf(body.getOrDefault("id", "")).trim(); + String printerName = String.valueOf(body.getOrDefault("printerName", "")).trim(); + String pdfBase64 = String.valueOf(body.getOrDefault("pdfBase64", "")).trim(); + String fileName = String.valueOf(body.getOrDefault("fileName", "")).trim(); + if (StringUtils.isBlank(id)) { + return Result.error("id 不能为空"); + } + MesXslRawMaterialCard card = mesXslRawMaterialCardService.getById(id); + if (card == null) { + return Result.error("未找到原材料卡片"); + } + String prefix = + StringUtils.isNotBlank(card.getBarcode()) ? card.getBarcode() : card.getId(); + String fn = + StringUtils.isNotBlank(fileName) ? fileName : ("原材料卡片-" + prefix + ".pdf"); + return printServerPdfJobService.submitPdfBase64( + printerName, pdfBase64, fn, "RAW_CARD_" + prefix); + } + /** * 通过id查询 */ diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/catalog/PrintBizTypeCatalog.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/catalog/PrintBizTypeCatalog.java new file mode 100644 index 0000000..4cb3c06 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/catalog/PrintBizTypeCatalog.java @@ -0,0 +1,73 @@ +package org.jeecg.modules.print.catalog; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.jeecg.modules.print.vo.PrintBizFieldItemVO; +import org.jeecg.modules.print.vo.PrintBizTypeVO; + +/** + * 业务类型及可映射字段目录(与实体 JSON 字段名一致,camelCase)。 + * 新增业务:在此注册 bizCode 与字段列表。 + */ +public final class PrintBizTypeCatalog { + + private static final Map REGISTRY = new LinkedHashMap<>(); + + static { + registerRawMaterialCard(); + } + + private PrintBizTypeCatalog() {} + + private static void registerRawMaterialCard() { + PrintBizTypeVO vo = new PrintBizTypeVO(); + vo.setBizCode("MES_RAW_MATERIAL_CARD"); + vo.setBizName("原材料卡片"); + vo.setDescription("mes_xsl_raw_material_card / MesXslRawMaterialCard"); + List fields = new ArrayList<>(); + fields.add(f("id", "主键")); + fields.add(f("barcode", "条码")); + fields.add(f("splitDetailId", "拆码明细行ID")); + fields.add(f("batchNo", "批次号")); + fields.add(f("entryDate", "入场日期")); + fields.add(f("materialId", "物料ID")); + fields.add(f("materialName", "物料名称")); + fields.add(f("materialDesc", "物料描述")); + fields.add(f("supplierId", "供应商ID")); + fields.add(f("supplierName", "供应商名称")); + fields.add(f("manufacturerMaterialName", "厂家物料名称")); + fields.add(f("shelfLife", "保质期")); + fields.add(f("totalWeight", "总重")); + fields.add(f("remainingWeight", "剩余重量")); + fields.add(f("remainingQuantity", "剩余数量")); + fields.add(f("status", "状态(字典)")); + fields.add(f("testResult", "检测结果(字典)")); + fields.add(f("warehouseArea", "库区/库位")); + fields.add(f("unloadOperator", "卸货操作人")); + fields.add(f("priorityPickup", "优先出库")); + fields.add(f("createBy", "创建人")); + fields.add(f("createTime", "创建时间")); + fields.add(f("updateBy", "更新人")); + fields.add(f("updateTime", "更新时间")); + vo.setFields(Collections.unmodifiableList(fields)); + REGISTRY.put(vo.getBizCode(), vo); + } + + private static PrintBizFieldItemVO f(String key, String label) { + return new PrintBizFieldItemVO(key, label, ""); + } + + public static List listAll() { + return new ArrayList<>(REGISTRY.values()); + } + + public static PrintBizTypeVO getByCode(String bizCode) { + if (bizCode == null) { + return null; + } + return REGISTRY.get(bizCode.trim()); + } +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/controller/PrintBizTemplateBindController.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/controller/PrintBizTemplateBindController.java new file mode 100644 index 0000000..2239253 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/controller/PrintBizTemplateBindController.java @@ -0,0 +1,216 @@ +package org.jeecg.modules.print.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 com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; +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.modules.print.catalog.PrintBizTypeCatalog; +import org.jeecg.modules.print.entity.PrintBizTemplateBind; +import org.jeecg.modules.print.entity.PrintTemplate; +import org.jeecg.modules.print.service.IPrintBizTemplateBindService; +import org.jeecg.modules.print.service.IPrintTemplateService; +import org.jeecg.modules.print.util.PrintBizDataMappingUtil; +import org.jeecg.modules.print.util.PrintNativeTemplateFieldExtractor; +import org.jeecg.modules.print.vo.PrintBizTypeVO; +import org.jeecg.modules.print.vo.PrintTemplateFieldItemVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +/** + * 业务与打印模板绑定:可视化配置字段映射 + */ +@Tag(name = "业务打印绑定") +@RestController +@RequestMapping("/print/bizTemplateBind") +public class PrintBizTemplateBindController extends JeecgController { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Autowired private IPrintTemplateService printTemplateService; + + @Operation(summary = "业务打印绑定-分页列表") + @GetMapping("/list") + @RequiresPermissions("print:bizBind:list") + public Result> list( + PrintBizTemplateBind query, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + HttpServletRequest req) { + QueryWrapper qw = + QueryGenerator.initQueryWrapper(query, req.getParameterMap()); + qw.orderByDesc("create_time"); + Page page = new Page<>(pageNo, pageSize); + return Result.OK(service.page(page, qw)); + } + + @AutoLog(value = "业务打印绑定-添加") + @Operation(summary = "业务打印绑定-添加") + @PostMapping("/add") + @RequiresPermissions("print:bizBind:add") + public Result add(@RequestBody PrintBizTemplateBind entity) { + String err = validateAndFillTemplate(entity); + if (err != null) { + return Result.error(err); + } + if (service.getByBizCode(entity.getBizCode()) != null) { + return Result.error("该业务编码已存在绑定,请编辑原记录或先删除"); + } + normalizeMappingJson(entity); + service.save(entity); + return Result.OK("添加成功"); + } + + @AutoLog(value = "业务打印绑定-编辑") + @Operation(summary = "业务打印绑定-编辑") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST}) + @RequiresPermissions("print:bizBind:edit") + public Result edit(@RequestBody PrintBizTemplateBind entity) { + PrintBizTemplateBind db = service.getById(entity.getId()); + if (db == null) { + return Result.error("记录不存在"); + } + String err = validateAndFillTemplate(entity); + if (err != null) { + return Result.error(err); + } + PrintBizTemplateBind other = service.getByBizCode(entity.getBizCode()); + if (other != null && !other.getId().equals(entity.getId())) { + return Result.error("业务编码与其他记录冲突"); + } + normalizeMappingJson(entity); + service.updateById(entity); + return Result.OK("编辑成功"); + } + + @AutoLog(value = "业务打印绑定-删除") + @Operation(summary = "业务打印绑定-删除") + @DeleteMapping("/delete") + @RequiresPermissions("print:bizBind:delete") + public Result delete(@RequestParam(name = "id") String id) { + service.removeById(id); + return Result.OK("删除成功"); + } + + @Operation(summary = "已注册的业务类型及字段目录") + @GetMapping("/bizTypes") + @RequiresPermissions("print:bizBind:list") + public Result> bizTypes() { + return Result.OK(PrintBizTypeCatalog.listAll()); + } + + @Operation(summary = "解析原生模板中的占位字段(bindField)") + @GetMapping("/parseTemplateFields") + @RequiresPermissions("print:bizBind:list") + public Result> parseTemplateFields( + @RequestParam(name = "templateId") String templateId) { + if (StringUtils.isBlank(templateId)) { + return Result.error("templateId 不能为空"); + } + PrintTemplate tpl = printTemplateService.getById(templateId); + if (tpl == null) { + return Result.error("模板不存在"); + } + List fields = + PrintNativeTemplateFieldExtractor.extract(tpl.getTemplateJson()); + return Result.OK(fields); + } + + @Operation(summary = "按业务编码查询绑定(供打印调用)") + @GetMapping("/queryByBizCode") + @RequiresPermissions("print:bizBind:list") + public Result queryByBizCode(@RequestParam(name = "bizCode") String bizCode) { + if (StringUtils.isBlank(bizCode)) { + return Result.error("bizCode 不能为空"); + } + PrintBizTemplateBind row = service.getByBizCode(bizCode.trim()); + if (row == null) { + return Result.error("未配置该业务的打印绑定"); + } + return Result.OK(row); + } + + @Operation(summary = "预览:业务数据按映射转为模板打印 JSON") + @PostMapping("/previewMappedData") + @RequiresPermissions("print:bizBind:list") + public Result> previewMappedData(@RequestBody PreviewMappedBody body) { + if (body == null || StringUtils.isBlank(body.getBizCode())) { + return Result.error("bizCode 不能为空"); + } + PrintBizTemplateBind bind = service.getByBizCode(body.getBizCode().trim()); + if (bind == null) { + return Result.error("未配置该业务的打印绑定"); + } + try { + ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson()); + JsonNode bizRoot = PrintBizDataMappingUtil.parseBizJson(body.getBizDataJson()); + ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping); + Map res = new HashMap<>(4); + res.put("templateCode", bind.getTemplateCode()); + res.put("templateId", bind.getTemplateId()); + res.put("printData", OBJECT_MAPPER.convertValue(printData, Map.class)); + return Result.OK(res); + } catch (Exception e) { + return Result.error("预览失败:" + e.getMessage()); + } + } + + /** @return 错误信息;null 表示校验通过 */ + private String validateAndFillTemplate(PrintBizTemplateBind entity) { + if (entity == null || StringUtils.isBlank(entity.getBizCode())) { + return "业务编码不能为空"; + } + if (StringUtils.isBlank(entity.getTemplateId())) { + return "请选择打印模板"; + } + PrintTemplate tpl = printTemplateService.getById(entity.getTemplateId()); + if (tpl == null) { + return "打印模板不存在"; + } + entity.setTemplateCode(tpl.getTemplateCode()); + if (PrintBizTypeCatalog.getByCode(entity.getBizCode()) != null + && StringUtils.isBlank(entity.getBizName())) { + entity.setBizName(PrintBizTypeCatalog.getByCode(entity.getBizCode()).getBizName()); + } + return null; + } + + /** 确保 mapping JSON 为数组字符串 */ + private void normalizeMappingJson(PrintBizTemplateBind entity) { + String raw = entity.getFieldMappingJson(); + if (StringUtils.isBlank(raw)) { + entity.setFieldMappingJson("[]"); + return; + } + try { + JsonNode n = OBJECT_MAPPER.readTree(raw); + if (!n.isArray()) { + entity.setFieldMappingJson("[]"); + } + } catch (Exception e) { + entity.setFieldMappingJson("[]"); + } + } + + @Data + public static class PreviewMappedBody { + private String bizCode; + /** 业务对象 JSON(对象或可解析字符串) */ + private Object bizDataJson; + } +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/controller/PrintTemplateController.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/controller/PrintTemplateController.java index d9ba191..4e904fa 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/controller/PrintTemplateController.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/controller/PrintTemplateController.java @@ -6,36 +6,19 @@ 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 java.util.ArrayList; import java.util.Base64; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.stream.Collectors; -import javax.print.Doc; -import javax.print.DocFlavor; -import javax.print.DocPrintJob; -import javax.print.PrintException; import javax.print.PrintService; -import javax.print.PrintServiceLookup; -import javax.print.SimpleDoc; -import javax.print.attribute.HashPrintRequestAttributeSet; -import javax.print.attribute.standard.JobName; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; -import java.awt.image.BufferedImage; import java.awt.print.PageFormat; import java.awt.print.Printable; -import java.awt.print.PrinterAbortException; import java.awt.print.PrinterException; import java.awt.print.PrinterJob; -import java.io.ByteArrayInputStream; import lombok.extern.slf4j.Slf4j; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.jeecg.common.api.vo.Result; @@ -44,8 +27,9 @@ import org.jeecg.common.system.base.controller.JeecgController; import org.jeecg.common.system.query.QueryGenerator; import org.jeecg.modules.print.entity.PrintTemplate; import org.jeecg.modules.print.service.IPrintTemplateService; +import org.jeecg.modules.print.support.PrintServerEnvironmentService; +import org.jeecg.modules.print.support.PrintServerPdfJobService; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; import org.jeecg.modules.print.ai.INativePrintTemplateImageAnalyzeService; import org.springframework.messaging.simp.SimpMessagingTemplate; @@ -59,8 +43,8 @@ import com.alibaba.fastjson.JSON; @RestController @RequestMapping("/print/template") public class PrintTemplateController extends JeecgController { - @Value("${print.network-printers:}") - private String networkPrinters; + @Autowired private PrintServerEnvironmentService printServerEnvironmentService; + @Autowired private PrintServerPdfJobService printServerPdfJobService; @Autowired private INativePrintTemplateImageAnalyzeService nativePrintTemplateImageAnalyzeService; @@ -225,45 +209,7 @@ public class PrintTemplateController extends JeecgController> queryPrinters() { - Map res = new HashMap<>(8); - List serverPrinters = new ArrayList<>(); - String serverDefaultPrinter = ""; - try { - PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null); - if (services != null) { - for (PrintService service : services) { - if (service != null && StringUtils.isNotBlank(service.getName())) { - serverPrinters.add(service.getName().trim()); - } - } - } - PrintService defaultService = PrintServiceLookup.lookupDefaultPrintService(); - if (defaultService != null && StringUtils.isNotBlank(defaultService.getName())) { - serverDefaultPrinter = defaultService.getName().trim(); - } - } catch (Exception e) { - log.warn("查询服务器打印机失败: {}", e.getMessage()); - } - List networkPrinterList = - StringUtils.isBlank(networkPrinters) - ? new ArrayList<>() - : java.util.Arrays.stream(networkPrinters.split(",")) - .map(String::trim) - .filter(StringUtils::isNotBlank) - .distinct() - .collect(Collectors.toList()); - - Map capability = new LinkedHashMap<>(4); - capability.put("localSupported", false); - capability.put("localReason", "浏览器环境无法直接枚举客户端本地打印机,需要本地组件或客户端程序配合。"); - capability.put("serverSupported", true); - capability.put("networkSupported", true); - - res.put("capability", capability); - res.put("serverPrinters", serverPrinters); - res.put("serverDefaultPrinter", serverDefaultPrinter); - res.put("networkPrinters", networkPrinterList); - return Result.OK(res); + return Result.OK(printServerEnvironmentService.buildPrinterQueryResult()); } @AutoLog(value = "打印模板-服务端直打") @@ -286,23 +232,12 @@ public class PrintTemplateController extends JeecgController 0) { - base64Body = pdfBase64.substring(commaIdx + 1); - } - byte[] pdfBytes = Base64.getDecoder().decode(base64Body); - String printJobName = StringUtils.isNotBlank(fileName) ? fileName : ("QH-MES-" + templateCode + ".pdf"); - // 优先直送 PDF 字节,避免走 RasterPrinterJob(虚拟打印机/无界面会话下易触发 PrinterAbortException) - if (tryPrintPdfBytesWithDocFlavor(target, pdfBytes, printJobName)) { - return Result.OK("已提交PDF到服务器打印机: " + resolvedPrinterLabel); - } - try (PDDocument document = PDDocument.load(new ByteArrayInputStream(pdfBytes))) { - PDFRenderer renderer = new PDFRenderer(document); - PrinterJob job = PrinterJob.getPrinterJob(); - job.setPrintService(target); - job.setJobName(printJobName); - job.setPrintable( - (graphics, pageFormat, pageIndex) -> { - if (pageIndex >= document.getNumberOfPages()) { - return Printable.NO_SUCH_PAGE; - } - BufferedImage image; - try { - image = renderer.renderImageWithDPI(pageIndex, 150); - } catch (Exception ex) { - throw new PrinterException("PDF页面渲染失败: " + ex.getMessage()); - } - Graphics2D g2 = (Graphics2D) graphics; - double imageableX = pageFormat.getImageableX(); - double imageableY = pageFormat.getImageableY(); - double imageableWidth = pageFormat.getImageableWidth(); - double imageableHeight = pageFormat.getImageableHeight(); - double scale = - Math.min(imageableWidth / image.getWidth(), imageableHeight / image.getHeight()); - int drawWidth = (int) Math.round(image.getWidth() * scale); - int drawHeight = (int) Math.round(image.getHeight() * scale); - int drawX = (int) Math.round(imageableX + (imageableWidth - drawWidth) / 2); - int drawY = (int) Math.round(imageableY + (imageableHeight - drawHeight) / 2); - g2.drawImage(image, drawX, drawY, drawWidth, drawHeight, null); - return Printable.PAGE_EXISTS; - }); - HashPrintRequestAttributeSet patts = new HashPrintRequestAttributeSet(); - patts.add(new JobName(printJobName, Locale.getDefault())); - job.print(patts); - } - return Result.OK("已提交PDF到服务器打印机: " + resolvedPrinterLabel); - } catch (PrinterAbortException e) { - log.error("PDF后端打印失败(PrinterAbortException)", e); - return Result.error(buildPdfPrinterAbortHint(printerName, lastResolvedPrinterLabel)); - } catch (Exception e) { - log.error("PDF后端打印失败", e); - return Result.error("PDF后端打印失败: " + e.getMessage()); - } - } - - /** - * 若打印机声明支持 application/pdf,则通过 DocPrintJob 提交,通常比 AWT 栅格化更稳定。 - */ - private boolean tryPrintPdfBytesWithDocFlavor(PrintService printService, byte[] pdfBytes, String jobName) { - DocFlavor flavor = new DocFlavor.INPUT_STREAM("application/pdf"); - if (!printService.isDocFlavorSupported(flavor)) { - return false; - } - try { - DocPrintJob docJob = printService.createPrintJob(); - ByteArrayInputStream in = new ByteArrayInputStream(pdfBytes); - Doc doc = new SimpleDoc(in, flavor, null); - HashPrintRequestAttributeSet attrs = new HashPrintRequestAttributeSet(); - if (StringUtils.isNotBlank(jobName)) { - attrs.add(new JobName(jobName, Locale.getDefault())); - } - docJob.print(doc, attrs); - return true; - } catch (PrintException e) { - log.warn("PDF DocFlavor 直送失败,将回退为位图渲染: {} - {}", printService.getName(), e.getMessage()); - return false; - } - } - - private static String buildPdfPrinterAbortHint(String requestedPrinterName, String resolvedPrintQueueName) { - StringBuilder sb = new StringBuilder(); - sb.append("打印任务被系统取消(PrinterAbortException)。常见原因:"); - sb.append("1) 默认或所选为「Microsoft Print to PDF」等虚拟打印机,在 Tomcat 等服务进程无交互桌面时无法弹出保存对话框,作业会被中止——请安装实体打印机并在前端指定 printerName;"); - sb.append("2) 打印机离线、队列暂停、缺纸或驱动报错;"); - sb.append("3) 运行服务的 Windows 账户无权访问打印队列。"); - if (StringUtils.isNotBlank(resolvedPrintQueueName)) { - sb.append(" 当前实际使用的打印队列: ").append(resolvedPrintQueueName.trim()).append("。"); - } - if (StringUtils.isNotBlank(requestedPrinterName) && !"__system_default__".equalsIgnoreCase(requestedPrinterName.trim())) { - sb.append(" 请求参数 printerName: ").append(requestedPrinterName.trim()).append("。"); - } - return sb.toString(); + return printServerPdfJobService.submitPdfBase64(printerName, pdfBase64, fileName, templateCode); } // ═══════════════════════════ 桌面端免密接口 ═══════════════════════════ @@ -491,26 +324,6 @@ public class PrintTemplateController extends JeecgController {} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/service/IPrintBizTemplateBindService.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/service/IPrintBizTemplateBindService.java new file mode 100644 index 0000000..dbfed8a --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/service/IPrintBizTemplateBindService.java @@ -0,0 +1,10 @@ +package org.jeecg.modules.print.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import org.jeecg.modules.print.entity.PrintBizTemplateBind; + +/** 业务打印模板绑定 */ +public interface IPrintBizTemplateBindService extends IService { + + PrintBizTemplateBind getByBizCode(String bizCode); +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/service/impl/PrintBizTemplateBindServiceImpl.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/service/impl/PrintBizTemplateBindServiceImpl.java new file mode 100644 index 0000000..68dd6ee --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/service/impl/PrintBizTemplateBindServiceImpl.java @@ -0,0 +1,24 @@ +package org.jeecg.modules.print.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.jeecg.modules.print.entity.PrintBizTemplateBind; +import org.jeecg.modules.print.mapper.PrintBizTemplateBindMapper; +import org.jeecg.modules.print.service.IPrintBizTemplateBindService; +import org.springframework.stereotype.Service; + +@Service +public class PrintBizTemplateBindServiceImpl extends ServiceImpl + implements IPrintBizTemplateBindService { + + @Override + public PrintBizTemplateBind getByBizCode(String bizCode) { + if (bizCode == null || bizCode.isBlank()) { + return null; + } + LambdaQueryWrapper q = new LambdaQueryWrapper<>(); + q.eq(PrintBizTemplateBind::getBizCode, bizCode.trim()); + q.last("LIMIT 1"); + return getOne(q); + } +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/support/PrintServerEnvironmentService.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/support/PrintServerEnvironmentService.java new file mode 100644 index 0000000..6f34893 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/support/PrintServerEnvironmentService.java @@ -0,0 +1,69 @@ +package org.jeecg.modules.print.support; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.print.PrintService; +import javax.print.PrintServiceLookup; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +/** + * 服务端打印机枚举能力(与 {@link org.jeecg.modules.print.controller.PrintTemplateController#queryPrinters} 一致)。 + */ +@Slf4j +@Service +public class PrintServerEnvironmentService { + + @Value("${print.network-printers:}") + private String networkPrinters; + + /** 与打印模板页 /print/template/queryPrinters 返回结构完全一致 */ + public Map buildPrinterQueryResult() { + Map res = new HashMap<>(8); + List serverPrinters = new ArrayList<>(); + String serverDefaultPrinter = ""; + try { + PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null); + if (services != null) { + for (PrintService service : services) { + if (service != null && StringUtils.isNotBlank(service.getName())) { + serverPrinters.add(service.getName().trim()); + } + } + } + PrintService defaultService = PrintServiceLookup.lookupDefaultPrintService(); + if (defaultService != null && StringUtils.isNotBlank(defaultService.getName())) { + serverDefaultPrinter = defaultService.getName().trim(); + } + } catch (Exception e) { + log.warn("查询服务器打印机失败: {}", e.getMessage()); + } + List networkPrinterList = + StringUtils.isBlank(networkPrinters) + ? new ArrayList<>() + : java.util.Arrays.stream(networkPrinters.split(",")) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .distinct() + .collect(Collectors.toList()); + + Map capability = new LinkedHashMap<>(4); + capability.put("localSupported", false); + capability.put( + "localReason", "浏览器环境无法直接枚举客户端本地打印机,需要本地组件或客户端程序配合。"); + capability.put("serverSupported", true); + capability.put("networkSupported", true); + + res.put("capability", capability); + res.put("serverPrinters", serverPrinters); + res.put("serverDefaultPrinter", serverDefaultPrinter); + res.put("networkPrinters", networkPrinterList); + return res; + } +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/support/PrintServerPdfJobService.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/support/PrintServerPdfJobService.java new file mode 100644 index 0000000..6d173c7 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/support/PrintServerPdfJobService.java @@ -0,0 +1,170 @@ +package org.jeecg.modules.print.support; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.awt.print.PageFormat; +import java.awt.print.Printable; +import java.awt.print.PrinterAbortException; +import java.awt.print.PrinterException; +import java.awt.print.PrinterJob; +import java.io.ByteArrayInputStream; +import java.util.Base64; +import java.util.Locale; +import javax.print.Doc; +import javax.print.DocFlavor; +import javax.print.DocPrintJob; +import javax.print.PrintException; +import javax.print.PrintService; +import javax.print.PrintServiceLookup; +import javax.print.SimpleDoc; +import javax.print.attribute.HashPrintRequestAttributeSet; +import javax.print.attribute.standard.JobName; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.rendering.PDFRenderer; +import org.jeecg.common.api.vo.Result; +import org.springframework.stereotype.Service; + +/** + * 服务端将 PDF Base64 提交至打印队列(与打印模板 {@code /print/template/directPrintPdf} 逻辑一致)。 + */ +@Slf4j +@Service +public class PrintServerPdfJobService { + + /** + * @param templateCodeOrPrefix 用于默认作业名:{@code QH-MES-{code}.pdf} + */ + public Result submitPdfBase64( + String printerName, String pdfBase64, String fileName, String templateCodeOrPrefix) { + if (StringUtils.isBlank(pdfBase64)) { + return Result.error("pdfBase64 不能为空"); + } + String lastResolvedPrinterLabel = null; + try { + PrintService target = resolvePrintService(printerName); + if (target == null) { + return Result.error("未找到可用打印机,请检查服务器打印机配置"); + } + final String resolvedPrinterLabel = target.getName(); + lastResolvedPrinterLabel = resolvedPrinterLabel; + String base64Body = pdfBase64; + int commaIdx = pdfBase64.indexOf(","); + if (pdfBase64.startsWith("data:") && commaIdx > 0) { + base64Body = pdfBase64.substring(commaIdx + 1); + } + byte[] pdfBytes = Base64.getDecoder().decode(base64Body); + String prefix = StringUtils.isNotBlank(templateCodeOrPrefix) ? templateCodeOrPrefix.trim() : "MES"; + String printJobName = + StringUtils.isNotBlank(fileName) ? fileName : ("QH-MES-" + prefix + ".pdf"); + if (tryPrintPdfBytesWithDocFlavor(target, pdfBytes, printJobName)) { + return Result.OK("已提交PDF到服务器打印机: " + resolvedPrinterLabel); + } + try (PDDocument document = PDDocument.load(new ByteArrayInputStream(pdfBytes))) { + PDFRenderer renderer = new PDFRenderer(document); + PrinterJob job = PrinterJob.getPrinterJob(); + job.setPrintService(target); + job.setJobName(printJobName); + job.setPrintable( + (graphics, pageFormat, pageIndex) -> { + if (pageIndex >= document.getNumberOfPages()) { + return Printable.NO_SUCH_PAGE; + } + BufferedImage image; + try { + image = renderer.renderImageWithDPI(pageIndex, 150); + } catch (Exception ex) { + throw new PrinterException("PDF页面渲染失败: " + ex.getMessage()); + } + Graphics2D g2 = (Graphics2D) graphics; + double imageableX = pageFormat.getImageableX(); + double imageableY = pageFormat.getImageableY(); + double imageableWidth = pageFormat.getImageableWidth(); + double imageableHeight = pageFormat.getImageableHeight(); + double scale = + Math.min(imageableWidth / image.getWidth(), imageableHeight / image.getHeight()); + int drawWidth = (int) Math.round(image.getWidth() * scale); + int drawHeight = (int) Math.round(image.getHeight() * scale); + int drawX = (int) Math.round(imageableX + (imageableWidth - drawWidth) / 2); + int drawY = (int) Math.round(imageableY + (imageableHeight - drawHeight) / 2); + g2.drawImage(image, drawX, drawY, drawWidth, drawHeight, null); + return Printable.PAGE_EXISTS; + }); + HashPrintRequestAttributeSet patts = new HashPrintRequestAttributeSet(); + patts.add(new JobName(printJobName, Locale.getDefault())); + job.print(patts); + } + return Result.OK("已提交PDF到服务器打印机: " + resolvedPrinterLabel); + } catch (PrinterAbortException e) { + log.error("PDF后端打印失败(PrinterAbortException)", e); + return Result.error(buildPdfPrinterAbortHint(printerName, lastResolvedPrinterLabel)); + } catch (Exception e) { + log.error("PDF后端打印失败", e); + return Result.error("PDF后端打印失败: " + e.getMessage()); + } + } + + public PrintService resolvePrintService(String printerName) { + PrintService target = null; + if (StringUtils.isNotBlank(printerName) && !"__system_default__".equals(printerName)) { + PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null); + if (services != null) { + for (PrintService serviceItem : services) { + if (serviceItem != null + && printerName.equalsIgnoreCase(String.valueOf(serviceItem.getName()).trim())) { + target = serviceItem; + break; + } + } + } + } + if (target == null) { + target = PrintServiceLookup.lookupDefaultPrintService(); + } + return target; + } + + private boolean tryPrintPdfBytesWithDocFlavor( + PrintService printService, byte[] pdfBytes, String jobName) { + DocFlavor flavor = new DocFlavor.INPUT_STREAM("application/pdf"); + if (!printService.isDocFlavorSupported(flavor)) { + return false; + } + try { + DocPrintJob docJob = printService.createPrintJob(); + ByteArrayInputStream in = new ByteArrayInputStream(pdfBytes); + Doc doc = new SimpleDoc(in, flavor, null); + HashPrintRequestAttributeSet attrs = new HashPrintRequestAttributeSet(); + if (StringUtils.isNotBlank(jobName)) { + attrs.add(new JobName(jobName, Locale.getDefault())); + } + docJob.print(doc, attrs); + return true; + } catch (PrintException e) { + log.warn( + "PDF DocFlavor 直送失败,将回退为位图渲染: {} - {}", + printService.getName(), + e.getMessage()); + return false; + } + } + + private static String buildPdfPrinterAbortHint( + String requestedPrinterName, String resolvedPrintQueueName) { + StringBuilder sb = new StringBuilder(); + sb.append("打印任务被系统取消(PrinterAbortException)。常见原因:"); + sb.append( + "1) 默认或所选为「Microsoft Print to PDF」等虚拟打印机,在 Tomcat 等服务进程无交互桌面时无法弹出保存对话框,作业会被中止——请安装实体打印机并在前端指定 printerName;"); + sb.append("2) 打印机离线、队列暂停、缺纸或驱动报错;"); + sb.append("3) 运行服务的 Windows 账户无权访问打印队列。"); + if (StringUtils.isNotBlank(resolvedPrintQueueName)) { + sb.append(" 当前实际使用的打印队列: ").append(resolvedPrintQueueName.trim()).append("。"); + } + if (StringUtils.isNotBlank(requestedPrinterName) + && !"__system_default__".equalsIgnoreCase(requestedPrinterName.trim())) { + sb.append(" 请求参数 printerName: ").append(requestedPrinterName.trim()).append("。"); + } + return sb.toString(); + } +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintBizDataMappingUtil.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintBizDataMappingUtil.java new file mode 100644 index 0000000..7bee392 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintBizDataMappingUtil.java @@ -0,0 +1,129 @@ +package org.jeecg.modules.print.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.apache.commons.lang3.StringUtils; + +/** 按映射规则把业务 JSON 转为模板打印数据(键为模板 bindField) */ +public final class PrintBizDataMappingUtil { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private PrintBizDataMappingUtil() {} + + public static ObjectNode mapBizToPrintData(JsonNode bizRoot, ArrayNode mappingRules) { + ObjectNode printData = MAPPER.createObjectNode(); + if (bizRoot == null || mappingRules == null) { + return printData; + } + for (JsonNode rule : mappingRules) { + if (rule == null || !rule.isObject()) { + continue; + } + String templateField = text(rule, "templateField"); + String bizField = text(rule, "bizField"); + if (StringUtils.isAnyBlank(templateField, bizField)) { + continue; + } + JsonNode val = resolvePath(bizRoot, bizField); + setPath(printData, templateField, val); + } + return printData; + } + + private static JsonNode resolvePath(JsonNode root, String path) { + if (root == null || StringUtils.isBlank(path)) { + return null; + } + String[] parts = path.split("\\."); + JsonNode cur = root; + for (String p : parts) { + if (cur == null || p.isEmpty()) { + return null; + } + cur = cur.get(p); + } + return cur; + } + + private static void setPath(ObjectNode target, String path, JsonNode value) { + if (target == null || StringUtils.isBlank(path)) { + return; + } + String[] parts = path.split("\\."); + if (parts.length == 1) { + putLeaf(target, parts[0], value); + return; + } + ObjectNode cur = target; + for (int i = 0; i < parts.length - 1; i++) { + String p = parts[i]; + JsonNode next = cur.get(p); + if (next == null || !next.isObject()) { + ObjectNode created = MAPPER.createObjectNode(); + cur.set(p, created); + cur = created; + } else { + cur = (ObjectNode) next; + } + } + String leaf = parts[parts.length - 1]; + putLeaf(cur, leaf, value); + } + + private static void putLeaf(ObjectNode node, String key, JsonNode value) { + if (value == null || value.isNull()) { + node.putNull(key); + } else if (value.isObject() || value.isArray()) { + node.set(key, value); + } else if (value.isTextual()) { + node.put(key, value.asText()); + } else if (value.isBoolean()) { + node.put(key, value.booleanValue()); + } else if (value.isNumber()) { + node.put(key, value.doubleValue()); + } else { + node.set(key, value); + } + } + + private static String text(JsonNode n, String key) { + if (n == null || !n.isObject()) { + return ""; + } + JsonNode v = n.get(key); + return v == null || v.isNull() ? "" : v.asText(""); + } + + /** 将任意 JsonNode 转为可序列化的 JSON 树(复制) */ + public static JsonNode parseBizJson(Object bizDataJson) throws Exception { + if (bizDataJson == null) { + return MAPPER.createObjectNode(); + } + if (bizDataJson instanceof String s) { + if (StringUtils.isBlank(s)) { + return MAPPER.createObjectNode(); + } + return MAPPER.readTree(s); + } + return MAPPER.valueToTree(bizDataJson); + } + + /** 规范化映射列表:解析字符串或数组 */ + public static ArrayNode parseMappingArray(String fieldMappingJson) throws Exception { + if (StringUtils.isBlank(fieldMappingJson)) { + return MAPPER.createArrayNode(); + } + JsonNode n = MAPPER.readTree(fieldMappingJson); + if (n.isArray()) { + return (ArrayNode) n; + } + return MAPPER.createArrayNode(); + } + + public static String mappingArrayToJson(ArrayNode arr) throws Exception { + return MAPPER.writeValueAsString(arr); + } +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintNativeTemplateFieldExtractor.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintNativeTemplateFieldExtractor.java new file mode 100644 index 0000000..3ff9035 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintNativeTemplateFieldExtractor.java @@ -0,0 +1,141 @@ +package org.jeecg.modules.print.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.jeecg.modules.print.vo.PrintTemplateFieldItemVO; + +/** + * 从原生打印模板 JSON 中收集所有 bindField / 表格列 field,供业务字段映射使用。 + * + *

原生设计器在 {@code dataBinding.params} 中维护「参数」列表(如 Parameter1~Parameter10),画布元素通过 + * bindField 引用这些 key;仅扫描 elements 会漏掉未拖放到画布上的参数,故必须先合并 dataBinding。 + */ +public final class PrintNativeTemplateFieldExtractor { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private PrintNativeTemplateFieldExtractor() {} + + public static List extract(String templateJson) { + List list = new ArrayList<>(); + if (StringUtils.isBlank(templateJson)) { + return list; + } + Set seen = new LinkedHashSet<>(); + try { + JsonNode root = MAPPER.readTree(templateJson); + // 优先收录设计器「参数/明细字段」目录,保证显示名与组件库一致,且不被后续元素扫描覆盖 + collectDataBinding(root, seen, list); + JsonNode elements = root.get("elements"); + if (elements != null && elements.isArray()) { + for (JsonNode el : elements) { + collectFromElement(el, seen, list); + } + } + } catch (Exception ignored) { + return list; + } + return list; + } + + /** 解析 schema.dataBinding:params(参数键)、detailTables(明细字段) */ + private static void collectDataBinding(JsonNode root, Set seen, List list) { + JsonNode db = root.get("dataBinding"); + if (db == null || !db.isObject()) { + return; + } + JsonNode params = db.get("params"); + if (params != null && params.isArray()) { + for (JsonNode p : params) { + if (p == null || !p.isObject()) { + continue; + } + String key = text(p, "key").trim(); + if (StringUtils.isBlank(key) || !seen.add(key)) { + continue; + } + list.add(new PrintTemplateFieldItemVO(key, "param", text(p, "label"))); + } + } + JsonNode detailTables = db.get("detailTables"); + if (detailTables != null && detailTables.isArray()) { + for (JsonNode t : detailTables) { + if (t == null || !t.isObject()) { + continue; + } + String tableKey = text(t, "tableKey").trim(); + JsonNode fields = t.get("fields"); + if (fields == null || !fields.isArray()) { + continue; + } + for (JsonNode f : fields) { + if (f == null || !f.isObject()) { + continue; + } + String fk = text(f, "key").trim(); + if (StringUtils.isBlank(fk)) { + continue; + } + // 与画布列 bindField 一致时多为短 key;多表明细同字段再加 tableKey 前缀消歧 + String bindKey = fk; + if (seen.contains(bindKey) && StringUtils.isNotBlank(tableKey)) { + bindKey = tableKey + "." + fk; + } + if (seen.contains(bindKey) || !seen.add(bindKey)) { + continue; + } + list.add(new PrintTemplateFieldItemVO(bindKey, "detailField", text(f, "label"))); + } + } + } + } + + private static void collectFromElement(JsonNode el, Set seen, List list) { + if (el == null || !el.isObject()) { + return; + } + String bindField = text(el, "bindField"); + if (StringUtils.isNotBlank(bindField) && seen.add(bindField.trim())) { + list.add( + new PrintTemplateFieldItemVO( + bindField.trim(), + text(el, "type"), + firstNonBlank(text(el, "title"), text(el, "text")))); + } + JsonNode cols = el.get("columns"); + if (cols != null && cols.isArray()) { + for (JsonNode c : cols) { + String field = firstNonBlank(text(c, "bindField"), text(c, "field")); + if (StringUtils.isNotBlank(field) && seen.add(field.trim())) { + list.add(new PrintTemplateFieldItemVO(field.trim(), "column", text(c, "title"))); + } + } + } + JsonNode nested = el.get("elements"); + if (nested != null && nested.isArray()) { + for (JsonNode child : nested) { + collectFromElement(child, seen, list); + } + } + } + + private static String text(JsonNode n, String key) { + if (n == null || !n.isObject()) { + return ""; + } + JsonNode v = n.get(key); + return v == null || v.isNull() ? "" : v.asText(""); + } + + private static String firstNonBlank(String a, String b) { + if (StringUtils.isNotBlank(a)) { + return a; + } + return StringUtils.isNotBlank(b) ? b : ""; + } +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizFieldItemVO.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizFieldItemVO.java new file mode 100644 index 0000000..f1b0f91 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizFieldItemVO.java @@ -0,0 +1,22 @@ +package org.jeecg.modules.print.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "业务实体可选字段") +public class PrintBizFieldItemVO implements Serializable { + @Schema(description = "字段路径(支持 a.b,与 JSON 一致)") + private String fieldKey; + + @Schema(description = "展示名称") + private String label; + + @Schema(description = "说明") + private String description; +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizTypeVO.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizTypeVO.java new file mode 100644 index 0000000..852f9ff --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizTypeVO.java @@ -0,0 +1,22 @@ +package org.jeecg.modules.print.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.Serializable; +import java.util.List; +import lombok.Data; + +@Data +@Schema(description = "可配置的业务类型") +public class PrintBizTypeVO implements Serializable { + @Schema(description = "业务编码") + private String bizCode; + + @Schema(description = "业务名称") + private String bizName; + + @Schema(description = "说明") + private String description; + + @Schema(description = "业务侧可用字段目录") + private List fields; +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintTemplateFieldItemVO.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintTemplateFieldItemVO.java new file mode 100644 index 0000000..8f14291 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintTemplateFieldItemVO.java @@ -0,0 +1,22 @@ +package org.jeecg.modules.print.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "模板中的数据占位字段") +public class PrintTemplateFieldItemVO implements Serializable { + @Schema(description = "模板 bindField 路径") + private String bindField; + + @Schema(description = "元素类型") + private String elementType; + + @Schema(description = "元素标题/提示") + private String titleHint; +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_50__print_biz_template_bind.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_50__print_biz_template_bind.sql new file mode 100644 index 0000000..793bc49 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_50__print_biz_template_bind.sql @@ -0,0 +1,43 @@ +-- 业务与打印模板绑定(字段映射可视化配置) +CREATE TABLE IF NOT EXISTS `print_biz_template_bind` ( + `id` varchar(36) NOT NULL COMMENT '主键', + `biz_code` varchar(64) NOT NULL COMMENT '业务编码(如 MES_RAW_MATERIAL_CARD)', + `biz_name` varchar(128) DEFAULT NULL COMMENT '业务名称(冗余展示)', + `template_id` varchar(36) NOT NULL COMMENT '打印模板主键', + `template_code` varchar(64) NOT NULL COMMENT '打印模板编码(冗余,便于调用方查询)', + `field_mapping_json` longtext COMMENT '字段映射 JSON:[{templateField,bizField}],templateField 对应模板 bindField', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `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 '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_print_biz_template_bind_biz` (`biz_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='业务打印模板绑定'; + +-- 菜单:打印管理下「业务打印绑定」 +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 '1900000000000000120', '1900000000000000100', '业务打印绑定', '/print/bizTemplateBind', 'print/bizTemplateBind/index', 1, 'PrintBizTemplateBind', NULL, 1, NULL, '0', 3.00, 0, 'ant-design:link-outlined', 1, 1, 0, 0, '业务与打印模板、字段映射配置', 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0 +FROM DUAL +WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000120'); + +-- 按钮权限 +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 '1900000000000000121', '1900000000000000120', '业务打印绑定-查询', NULL, NULL, 0, NULL, NULL, 2, 'print:bizBind:list', '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` = '1900000000000000121'); + +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 '1900000000000000122', '1900000000000000120', '业务打印绑定-添加', NULL, NULL, 0, NULL, NULL, 2, 'print:bizBind:add', '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` = '1900000000000000122'); + +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 '1900000000000000123', '1900000000000000120', '业务打印绑定-编辑', NULL, NULL, 0, NULL, NULL, 2, 'print:bizBind:edit', '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` = '1900000000000000123'); + +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 '1900000000000000124', '1900000000000000120', '业务打印绑定-删除', NULL, NULL, 0, NULL, NULL, 2, 'print:bizBind:delete', '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` = '1900000000000000124'); diff --git a/jeecgboot-vue3/src/views/print/bizTemplateBind/bizTemplateBind.api.ts b/jeecgboot-vue3/src/views/print/bizTemplateBind/bizTemplateBind.api.ts new file mode 100644 index 0000000..056f26e --- /dev/null +++ b/jeecgboot-vue3/src/views/print/bizTemplateBind/bizTemplateBind.api.ts @@ -0,0 +1,29 @@ +import { defHttp } from '/@/utils/http/axios'; + +enum Api { + list = '/print/bizTemplateBind/list', + add = '/print/bizTemplateBind/add', + edit = '/print/bizTemplateBind/edit', + deleteOne = '/print/bizTemplateBind/delete', + bizTypes = '/print/bizTemplateBind/bizTypes', + parseTemplateFields = '/print/bizTemplateBind/parseTemplateFields', + previewMappedData = '/print/bizTemplateBind/previewMappedData', +} + +export const list = (params) => defHttp.get({ url: Api.list, params }); +// 与系统其它模块一致:body 走 params 键 +export const add = (params) => defHttp.post({ url: Api.add, params }); +export const edit = (params) => defHttp.put({ url: Api.edit, params }); +export const deleteOne = (params, handleSuccess?) => + defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => handleSuccess?.()); + +export const bizTypes = () => defHttp.get({ url: Api.bizTypes }); +export const parseTemplateFields = (templateId: string) => + defHttp.get({ + url: Api.parseTemplateFields, + params: { templateId, _t: Date.now() }, + }); + +/** 预览映射后的打印数据 */ +export const previewMappedData = (data: { bizCode: string; bizDataJson: Record }) => + defHttp.post({ url: Api.previewMappedData, data }); diff --git a/jeecgboot-vue3/src/views/print/bizTemplateBind/bizTemplateBind.data.ts b/jeecgboot-vue3/src/views/print/bizTemplateBind/bizTemplateBind.data.ts new file mode 100644 index 0000000..d18ed58 --- /dev/null +++ b/jeecgboot-vue3/src/views/print/bizTemplateBind/bizTemplateBind.data.ts @@ -0,0 +1,8 @@ +import type { BasicColumn } from '/@/components/Table'; + +export const columns: BasicColumn[] = [ + { title: '业务编码', dataIndex: 'bizCode', width: 200 }, + { title: '业务名称', dataIndex: 'bizName', width: 140 }, + { title: '模板编码', dataIndex: 'templateCode', width: 180 }, + { title: '备注', dataIndex: 'remark', ellipsis: true }, +]; diff --git a/jeecgboot-vue3/src/views/print/bizTemplateBind/index.vue b/jeecgboot-vue3/src/views/print/bizTemplateBind/index.vue new file mode 100644 index 0000000..2a5f2a3 --- /dev/null +++ b/jeecgboot-vue3/src/views/print/bizTemplateBind/index.vue @@ -0,0 +1,414 @@ + + + + + diff --git a/jeecgboot-vue3/src/views/print/template/utils/printDotBridge.ts b/jeecgboot-vue3/src/views/print/template/utils/printDotBridge.ts index 25f34d0..5f28947 100644 --- a/jeecgboot-vue3/src/views/print/template/utils/printDotBridge.ts +++ b/jeecgboot-vue3/src/views/print/template/utils/printDotBridge.ts @@ -92,6 +92,20 @@ function enhancePrintDotErrorMessage(raw: string): string { if (/SumatraPDF\.exe not found/i.test(m) || /SUMATRAPDF_PATH/i.test(m)) { return `${m}。本地处理:PrintDot 依赖 SumatraPDF 静默打印 PDF。请安装 Sumatra PDF 后任选其一:将 SumatraPDF.exe 放在 PrintDot 客户端 exe 同目录;或将 Sumatra 安装目录加入系统 PATH;或设置用户/系统环境变量 SUMATRAPDF_PATH 指向 SumatraPDF.exe 的完整路径,然后重启 PrintDot。`; } + /** 桥接端在等待 Windows 打印队列接受作业(默认约 2 分钟)未果 */ + if (/not queued/i.test(m) || /Printed\s+0\s*\/\s*\d+\s+copies/i.test(m)) { + return `${m} + +【说明】PrintDot 已通过 SumatraPDF 发起静默打印,但在约定时间内未检测到作业进入系统打印队列。 + +【建议逐项排查】 +1. 打印机是否开机、联网(网络打印机)、线缆/USB 是否正常。 +2. Windows「设备和打印机」中该打印机是否就绪、无暂停;打印队列里是否有卡住的任务(可先清空队列)。 +3. 下拉选择的打印机名称是否与系统完全一致(可在本页「刷新打印机」后重选)。 +4. 重启「Print Spooler」打印后台服务,或重启 PrintDot 客户端后再试。 +5. 模板版面过大时生成的 PDF 体积大,可能导致 Sumatra 处理变慢——可先简化模板或缩小画布后再试。 +6. 若频繁超时,需在 PrintDot 桌面端放宽「队列确认」超时(该 2 分钟由客户端决定,浏览器无法修改)。`; + } return m; } diff --git a/jeecgboot-vue3/src/views/print/template/utils/printHtmlToPdfBase64.ts b/jeecgboot-vue3/src/views/print/template/utils/printHtmlToPdfBase64.ts index b762067..98bd5ac 100644 --- a/jeecgboot-vue3/src/views/print/template/utils/printHtmlToPdfBase64.ts +++ b/jeecgboot-vue3/src/views/print/template/utils/printHtmlToPdfBase64.ts @@ -56,6 +56,11 @@ export type BuildPdfFromHtmlOptions = { * false:整张版面压成一页 PDF(长图一页,一般仅特殊场景使用)。 */ paginate?: boolean; + /** + * 是否严格使用入参纸张尺寸(默认 false 保持历史行为)。 + * 原生模板桥接打印建议开启,避免内容测量误差把小标签纸扩成 A4。 + */ + exactPaperSize?: boolean; }; /** @@ -70,6 +75,7 @@ export async function buildPdfBase64FromHtmlFragment( options: BuildPdfFromHtmlOptions = {}, ): Promise { const paginate = options.paginate !== false; + const exactPaperSize = options.exactPaperSize === true; const [{ jsPDF }, html2canvasModule] = await Promise.all([import('jspdf'), import('html2canvas')]); const html2canvas = html2canvasModule.default; const container = document.createElement('div'); @@ -152,10 +158,11 @@ export async function buildPdfBase64FromHtmlFragment( const pad = 1; if (paginate) { - const sheetW = Math.max(widthMm, pxToMm(sw) + pad); + const sheetW = exactPaperSize ? Math.max(1, widthMm) : Math.max(widthMm, pxToMm(sw) + pad); const sheetH = Math.max(1, heightMm); const sliceH = Math.max(1, Math.round(mmToPx(sheetH) * scale)); - const pdf = new jsPDF({ unit: 'mm', format: [sheetW, sheetH] }); + const orientation = sheetW > sheetH ? 'landscape' : 'portrait'; + const pdf = new jsPDF({ unit: 'mm', orientation, format: [sheetW, sheetH] }); let y = 0; let first = true; /** 余量不足一页高的 2% 时视为测量噪声,避免多出一页空白 */ @@ -193,7 +200,7 @@ export async function buildPdfBase64FromHtmlFragment( // 单页长图模式(paginate: false) const contentWidthMm = pxToMm(sw); const contentHeightMm = pxToMm(sh); - const minW = Math.max(widthMm, contentWidthMm) + pad; + const minW = exactPaperSize ? Math.max(1, widthMm) : Math.max(widthMm, contentWidthMm) + pad; const minH = Math.max(heightMm, contentHeightMm) + pad; const canvasRatio = cw / ch; let pdfH = Math.max(minH, minW / canvasRatio); @@ -202,7 +209,8 @@ export async function buildPdfBase64FromHtmlFragment( pdfW = minW; pdfH = pdfW / canvasRatio; } - const pdf = new jsPDF({ unit: 'mm', format: [pdfW, pdfH] }); + const orientation = pdfW > pdfH ? 'landscape' : 'portrait'; + const pdf = new jsPDF({ unit: 'mm', orientation, format: [pdfW, pdfH] }); const imgData = canvas.toDataURL('image/jpeg', 0.92); pdf.addImage(imgData, 'JPEG', 0, 0, pdfW, pdfH); return arrayBufferToBase64(pdf.output('arraybuffer')); diff --git a/jeecgboot-vue3/src/views/print/template/utils/printNativeViaPrintDot.ts b/jeecgboot-vue3/src/views/print/template/utils/printNativeViaPrintDot.ts index d18e967..9154838 100644 --- a/jeecgboot-vue3/src/views/print/template/utils/printNativeViaPrintDot.ts +++ b/jeecgboot-vue3/src/views/print/template/utils/printNativeViaPrintDot.ts @@ -20,13 +20,14 @@ export async function printNativeSchemaViaPrintDot(params: { const inner = extractBodyInnerHtmlFromFullDocument(fullHtml); const pdfBase64 = await buildPdfBase64FromHtmlFragment(inner, params.schema.page.width, params.schema.page.height, { paginate: true, + exactPaperSize: true, }); const printers = await fetchPrintDotPrinters(); const fromStore = params.printerSelection ?? localStorage.getItem(PRINT_TEMPLATE_SELECTED_PRINTER_KEY) ?? '__system_default__'; const resolved = resolvePrintDotPrinterName(fromStore, printers); if (!resolved) { - throw new Error('未解析到可用打印机:请在模板列表选择打印机,或启动 PrintDot 后刷新打印机列表'); + throw new Error('未解析到可用打印机:请在本页或打印模板页选择打印机,并确保本机 PrintDot 已启动后刷新打印机列表'); } const result = await printDotSendPdf({ printer: resolved, diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCard.api.ts b/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCard.api.ts index 26456ea..4828124 100644 --- a/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCard.api.ts +++ b/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCard.api.ts @@ -12,6 +12,10 @@ enum Api { importExcel = '/xslmes/mesXslRawMaterialCard/importExcel', exportXls = '/xslmes/mesXslRawMaterialCard/exportXls', updatePriority = '/xslmes/mesXslRawMaterialCard/updatePriority', + /** 与打印模板页 queryPrinters 返回结构一致 */ + queryPrinters = '/xslmes/mesXslRawMaterialCard/queryPrinters', + prepareNativePrint = '/xslmes/mesXslRawMaterialCard/prepareNativePrint', + printPdf = '/xslmes/mesXslRawMaterialCard/printPdf', } export const getExportUrl = Api.exportXls; @@ -47,3 +51,15 @@ export const saveOrUpdate = (params, isUpdate) => { export const updatePriority = (id: string, priorityPickup: string) => defHttp.put({ url: Api.updatePriority, params: { id, priorityPickup } }, { joinParamsToUrl: true }); + +export const queryPrinters = () => defHttp.get({ url: Api.queryPrinters }); + +export const prepareNativePrint = (id: string) => + defHttp.get({ + url: Api.prepareNativePrint, + params: { id, _t: Date.now() }, + }); + +/** id + 前端生成的 pdfBase64;printerName 空则用默认队列 */ +export const printPdf = (data: { id: string; printerName?: string; pdfBase64: string; fileName?: string }) => + defHttp.post({ url: Api.printPdf, data, timeout: 3 * 60 * 1000 }); diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCardList.vue b/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCardList.vue index 797888d..97bdd46 100644 --- a/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCardList.vue +++ b/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCardList.vue @@ -7,6 +7,51 @@ 新增 导出 导入 + + PrintDot 桥接 + + + + 下载打印插件 + + 刷新打印机 + + 添加 + + + 打印选中 + + + diff --git a/yy-admin-master/YY.Admin.Core/Core/Services/IRawMaterialCardService.cs b/yy-admin-master/YY.Admin.Core/Core/Services/IRawMaterialCardService.cs index 2f57a6c..7da48be 100644 --- a/yy-admin-master/YY.Admin.Core/Core/Services/IRawMaterialCardService.cs +++ b/yy-admin-master/YY.Admin.Core/Core/Services/IRawMaterialCardService.cs @@ -23,4 +23,10 @@ public interface IRawMaterialCardService /// 为 true 时仅返回匹配数量、不真正删除(用于「重新拆码」弹窗预提示) /// 匹配/删除的卡片数量;失败返回 -1 Task DeleteBySplitDetailIdsAsync(IEnumerable splitDetailIds, bool dryRun = false, CancellationToken ct = default); + + ///

+ /// 按业务打印绑定生成模板 JSON + 映射后 printData,供桌面端打印预览使用。 + /// + /// (templateJson, printDataJson, errorMessage) 元组;errorMessage 非 null 时表示失败 + Task<(string templateJson, string printDataJson, string? errorMessage)> PrepareNativePrintAsync(string id, CancellationToken ct = default); } diff --git a/yy-admin-master/YY.Admin.Services/Service/RawMaterialCard/RawMaterialCardService.cs b/yy-admin-master/YY.Admin.Services/Service/RawMaterialCard/RawMaterialCardService.cs index 1cd3d3b..9c79fb2 100644 --- a/yy-admin-master/YY.Admin.Services/Service/RawMaterialCard/RawMaterialCardService.cs +++ b/yy-admin-master/YY.Admin.Services/Service/RawMaterialCard/RawMaterialCardService.cs @@ -317,6 +317,39 @@ public class RawMaterialCardService : IRawMaterialCardService, ISingletonDepende } } + public async Task<(string templateJson, string printDataJson, string? errorMessage)> PrepareNativePrintAsync(string id, CancellationToken ct = default) + { + if (!_networkMonitor.IsOnline) + return (string.Empty, "{}", "当前离线,无法获取打印数据"); + try + { + var url = $"{BaseUrl}/xslmes/mesXslRawMaterialCard/anon/prepareNativePrint?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}"; + using var client = CreateClient(); + var resp = await client.GetAsync(url, ct).ConfigureAwait(false); + var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + if (!root.TryGetProperty("code", out var codeEl) || codeEl.GetInt32() != 200) + { + var msg = root.TryGetProperty("message", out var msgEl) ? msgEl.GetString() : "未知错误"; + return (string.Empty, "{}", msg ?? "服务端返回错误"); + } + var result = root.GetProperty("result"); + var templateJson = result.TryGetProperty("templateJson", out var tjEl) ? tjEl.GetString() : null; + var printDataJson = result.TryGetProperty("printData", out var pdEl) + ? pdEl.GetRawText() + : "{}"; + if (string.IsNullOrWhiteSpace(templateJson)) + return (string.Empty, "{}", "服务端未返回模板 JSON,请先在「业务打印绑定」中配置原材料卡片"); + return (templateJson!, printDataJson, null); + } + catch (Exception ex) + { + _logger.Warning($"[原材料卡片] 准备打印数据失败 id={id}: {ex.Message}"); + return (string.Empty, "{}", $"获取打印数据失败:{ex.Message}"); + } + } + public async Task UpdatePriorityAsync(string id, string priorityPickup, CancellationToken ct = default) { if (_networkMonitor.IsOnline) diff --git a/yy-admin-master/YY.Admin/ViewModels/RawMaterialCard/RawMaterialCardListViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/RawMaterialCard/RawMaterialCardListViewModel.cs index 344b63d..04ba6d6 100644 --- a/yy-admin-master/YY.Admin/ViewModels/RawMaterialCard/RawMaterialCardListViewModel.cs +++ b/yy-admin-master/YY.Admin/ViewModels/RawMaterialCard/RawMaterialCardListViewModel.cs @@ -5,11 +5,12 @@ using System.Collections.ObjectModel; using System.Diagnostics; using System.Windows; using YY.Admin.Core; +using YY.Admin.Core.Entity; using YY.Admin.Core.Events; using YY.Admin.Core.Helper; -using YY.Admin.Core.Entity; using YY.Admin.Core.Services; using YY.Admin.Services.Service; +using YY.Admin.Views.Print; using YY.Admin.Views.RawMaterialCard; namespace YY.Admin.ViewModels.RawMaterialCard; @@ -18,6 +19,7 @@ public class RawMaterialCardListViewModel : BaseViewModel { private readonly IRawMaterialCardService _cardService; private readonly IJeecgDictSyncService _dictSyncService; + private readonly IPrintDotService _printDotService; private SubscriptionToken? _changedToken; private SubscriptionToken? _syncConflictToken; @@ -60,6 +62,7 @@ public class RawMaterialCardListViewModel : BaseViewModel public DelegateCommand AddCommand { get; } public DelegateCommand EditCommand { get; } public DelegateCommand DeleteCommand { get; } + public DelegateCommand PrintCommand { get; } public DelegateCommand TogglePriorityCommand { get; } public DelegateCommand PrevPageCommand { get; } public DelegateCommand NextPageCommand { get; } @@ -67,11 +70,13 @@ public class RawMaterialCardListViewModel : BaseViewModel public RawMaterialCardListViewModel( IRawMaterialCardService cardService, IJeecgDictSyncService dictSyncService, + IPrintDotService printDotService, IContainerExtension container, IRegionManager regionManager) : base(container, regionManager) { _cardService = cardService; _dictSyncService = dictSyncService; + _printDotService = printDotService; SearchCommand = new DelegateCommand(async () => { PageNo = 1; await LoadAsync(); }); ResetCommand = new DelegateCommand(async () => @@ -83,6 +88,7 @@ public class RawMaterialCardListViewModel : BaseViewModel AddCommand = new DelegateCommand(async () => await ShowAddDialogAsync()); EditCommand = new DelegateCommand(async c => await ShowEditDialogAsync(c)); DeleteCommand = new DelegateCommand(async c => await DeleteAsync(c)); + PrintCommand = new DelegateCommand(async c => await ShowPrintPreviewAsync(c)); TogglePriorityCommand = new DelegateCommand(async c => await TogglePriorityAsync(c)); PrevPageCommand = new DelegateCommand(async () => { if (PageNo > 1) { PageNo--; await LoadAsync(); } }); NextPageCommand = new DelegateCommand(async () => { if ((long)PageNo * PageSize < Total) { PageNo++; await LoadAsync(); } }); @@ -255,6 +261,65 @@ public class RawMaterialCardListViewModel : BaseViewModel } } + private async Task ShowPrintPreviewAsync(MesXslRawMaterialCard card) + { + if (card?.Id == null) return; + try + { + IsLoading = true; + var (templateJson, printDataJson, errorMessage) = await _cardService.PrepareNativePrintAsync(card.Id); + if (errorMessage != null) + { + Growl.Error(errorMessage); + return; + } + + // 构造一个最简的 PrintTemplate 对象用于传入 PrintPreviewWindow(供显示标题 / 纸张信息) + var tpl = BuildPrintTemplateFromJson(templateJson); + + var win = new PrintPreviewWindow(tpl, templateJson, _printDotService, null, printDataJson) + { + Owner = Application.Current.MainWindow, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + }; + win.Show(); + } + catch (Exception ex) + { + Growl.Error($"打开打印预览失败:{ex.Message}"); + } + finally + { + IsLoading = false; + } + } + + private static PrintTemplate BuildPrintTemplateFromJson(string templateJson) + { + try + { + var root = System.Text.Json.JsonDocument.Parse(templateJson).RootElement; + double w = 210, h = 297; + if (root.TryGetProperty("page", out var page)) + { + if (page.TryGetProperty("width", out var wEl)) w = wEl.GetDouble(); + if (page.TryGetProperty("height", out var hEl)) h = hEl.GetDouble(); + } + return new PrintTemplate + { + TemplateName = "原材料卡片", + TemplateCode = "MES_RAW_MATERIAL_CARD", + PaperWidthMm = w, + PaperHeightMm = h, + PaperOrientation = w > h ? "横向" : "纵向", + }; + } + catch + { + return new PrintTemplate { TemplateName = "原材料卡片", TemplateCode = "MES_RAW_MATERIAL_CARD" }; + } + } + protected override void CleanUp() { base.CleanUp(); diff --git a/yy-admin-master/YY.Admin/Views/Print/PrintPreviewWindow.xaml.cs b/yy-admin-master/YY.Admin/Views/Print/PrintPreviewWindow.xaml.cs index 76f7d72..cfbe240 100644 --- a/yy-admin-master/YY.Admin/Views/Print/PrintPreviewWindow.xaml.cs +++ b/yy-admin-master/YY.Admin/Views/Print/PrintPreviewWindow.xaml.cs @@ -23,7 +23,8 @@ public partial class PrintPreviewWindow : HandyControl.Controls.Window } public PrintPreviewWindow(PrintTemplate template, string? templateJson, - IPrintDotService? printDotService, string? selectedPrinterName) + IPrintDotService? printDotService, string? selectedPrinterName, + string? initialParamJson = null) { InitializeComponent(); _template = template; @@ -36,7 +37,9 @@ public partial class PrintPreviewWindow : HandyControl.Controls.Window $"尺寸:{template.PaperWidthMm ?? 210}×{template.PaperHeightMm ?? 297} mm " + $"方向:{template.PaperOrientation ?? "纵向"}"; - TbParamJson.Text = BuildMockParamJson(_templateJson); + TbParamJson.Text = !string.IsNullOrWhiteSpace(initialParamJson) + ? initialParamJson! + : BuildMockParamJson(_templateJson); // 没有 PrintDot 服务时禁用打印相关按钮 if (_printDotService == null) diff --git a/yy-admin-master/YY.Admin/Views/RawMaterialCard/RawMaterialCardListView.xaml b/yy-admin-master/YY.Admin/Views/RawMaterialCard/RawMaterialCardListView.xaml index 38f8832..1162e1c 100644 --- a/yy-admin-master/YY.Admin/Views/RawMaterialCard/RawMaterialCardListView.xaml +++ b/yy-admin-master/YY.Admin/Views/RawMaterialCard/RawMaterialCardListView.xaml @@ -143,7 +143,7 @@ - + @@ -152,6 +152,11 @@ FontSize="12" Height="26" Padding="8,0" Command="{Binding DataContext.EditCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}" CommandParameter="{Binding}"/> +