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 @@
+
+
+
+
+ 新增绑定
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 解析模板占位字段
+
+
+ 同名自动匹配
+
+
+
+
+
+
+
+ 映射预览(可选)
+
+ 生成打印数据预览
+ {{ previewResult }}
+
+
+
+
+
+
+
+
+
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 桥接
+
+
+
+ 下载打印插件
+
+ refreshPrinterOptions(true)">刷新打印机
+
+ 添加
+
+
+ 打印选中
+
@@ -42,20 +87,73 @@
+
+
+
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}"/>
+