新增打印业务绑定功能,整合原材料卡片和入场记录的打印模板配置,优化打印数据准备逻辑。新增打印机查询接口,提升打印服务的灵活性和用户体验。同时,重构相关控制器以支持新的打印常量定义,增强系统的可维护性和扩展性。
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
package org.jeecg.modules.xslmes.constant;
|
||||
|
||||
/**
|
||||
* 打印业务绑定 biz_code 使用菜单 permission id;与 print_biz_perm_entity、Flyway 中原材料卡片菜单一致。
|
||||
*/
|
||||
public final class MesXslPrintConstants {
|
||||
|
||||
/** 原材料卡片页面菜单(sys_permission.id) */
|
||||
public static final String RAW_MATERIAL_CARD_PERM_ID = "1900000000000000540";
|
||||
|
||||
/** 原料入场记录页面菜单(sys_permission.id,与 Flyway 中 parent 菜单一致) */
|
||||
public static final String RAW_MATERIAL_ENTRY_PERM_ID = "1900000000000000530";
|
||||
|
||||
private MesXslPrintConstants() {}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ 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.constant.MesXslPrintConstants;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslCustomer;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialEntry;
|
||||
@@ -620,8 +621,6 @@ 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")
|
||||
@@ -629,7 +628,8 @@ public class MesXslDesktopAnonController {
|
||||
try {
|
||||
MesXslRawMaterialCard card = rawMaterialCardService.getById(id);
|
||||
if (card == null) return Result.error("未找到原材料卡片");
|
||||
PrintBizTemplateBind bind = printBizTemplateBindService.getByBizCode(RAW_MATERIAL_CARD_BIZ_CODE);
|
||||
PrintBizTemplateBind bind =
|
||||
printBizTemplateBindService.getByBizCode(MesXslPrintConstants.RAW_MATERIAL_CARD_PERM_ID);
|
||||
if (bind == null) return Result.error("请先在「业务打印绑定」中配置原材料卡片与打印模板");
|
||||
PrintTemplate tpl = printTemplateService.getById(bind.getTemplateId());
|
||||
if (tpl == null) return Result.error("绑定的打印模板不存在");
|
||||
@@ -648,6 +648,32 @@ public class MesXslDesktopAnonController {
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "原料入场记录-免密准备原生打印数据(桌面端用)")
|
||||
@GetMapping("/xslmes/mesXslRawMaterialEntry/anon/prepareNativePrint")
|
||||
public Result<Map<String, Object>> rawMaterialEntryAnonPrepareNativePrint(@RequestParam(name = "id") String id) {
|
||||
try {
|
||||
MesXslRawMaterialEntry entry = rawMaterialEntryService.getById(id);
|
||||
if (entry == null) return Result.error("未找到原料入场记录");
|
||||
PrintBizTemplateBind bind =
|
||||
printBizTemplateBindService.getByBizCode(MesXslPrintConstants.RAW_MATERIAL_ENTRY_PERM_ID);
|
||||
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(entry);
|
||||
ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping);
|
||||
Map<String, Object> out = new HashMap<>(8);
|
||||
out.put("entryId", entry.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 = "仓库-免密分页列表查询(供桌面端筛选使用)")
|
||||
|
||||
@@ -29,6 +29,7 @@ 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.constant.MesXslPrintConstants;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialCardService;
|
||||
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
|
||||
@@ -56,9 +57,6 @@ import org.springframework.web.servlet.ModelAndView;
|
||||
@Slf4j
|
||||
public class MesXslRawMaterialCardController extends JeecgController<MesXslRawMaterialCard, IMesXslRawMaterialCardService> {
|
||||
|
||||
/** 与 PrintBizTypeCatalog / 业务打印绑定一致 */
|
||||
private static final String RAW_MATERIAL_CARD_BIZ_CODE = "MES_RAW_MATERIAL_CARD";
|
||||
|
||||
@Autowired
|
||||
private IMesXslRawMaterialCardService mesXslRawMaterialCardService;
|
||||
@Autowired
|
||||
@@ -184,7 +182,7 @@ public class MesXslRawMaterialCardController extends JeecgController<MesXslRawMa
|
||||
return Result.error("未找到原材料卡片");
|
||||
}
|
||||
PrintBizTemplateBind bind =
|
||||
printBizTemplateBindService.getByBizCode(RAW_MATERIAL_CARD_BIZ_CODE);
|
||||
printBizTemplateBindService.getByBizCode(MesXslPrintConstants.RAW_MATERIAL_CARD_PERM_ID);
|
||||
if (bind == null) {
|
||||
return Result.error("请先在「业务打印绑定」中配置原材料卡片与打印模板");
|
||||
}
|
||||
|
||||
@@ -1,28 +1,50 @@
|
||||
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.MesXslRawMaterialEntry;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialEntryService;
|
||||
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
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.constant.MesXslPrintConstants;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialEntry;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialEntryService;
|
||||
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: 原料入场记录
|
||||
@@ -40,6 +62,16 @@ public class MesXslRawMaterialEntryController extends JeecgController<MesXslRawM
|
||||
private IMesXslRawMaterialEntryService mesXslRawMaterialEntryService;
|
||||
@Autowired
|
||||
private MesXslStompNotifyService stompNotify;
|
||||
@Autowired
|
||||
private PrintServerEnvironmentService printServerEnvironmentService;
|
||||
@Autowired
|
||||
private PrintServerPdfJobService printServerPdfJobService;
|
||||
@Autowired
|
||||
private IPrintBizTemplateBindService printBizTemplateBindService;
|
||||
@Autowired
|
||||
private IPrintTemplateService printTemplateService;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Operation(summary = "原料入场记录-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
@@ -115,6 +147,78 @@ public class MesXslRawMaterialEntryController extends JeecgController<MesXslRawM
|
||||
return Result.OK(mesXslRawMaterialEntry);
|
||||
}
|
||||
|
||||
@Operation(summary = "原料入场记录-查询可用打印机")
|
||||
@GetMapping(value = "/queryPrinters")
|
||||
@RequiresPermissions(
|
||||
value = {"xslmes:mes_xsl_raw_material_entry:list", "xslmes:mes_xsl_raw_material_entry:edit"},
|
||||
logical = Logical.OR)
|
||||
public Result<Map<String, Object>> queryPrinters() {
|
||||
return Result.OK(printServerEnvironmentService.buildPrinterQueryResult());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据业务打印绑定生成模板 JSON + 映射后的 printData,供前端生成 PDF 后调用 printPdf
|
||||
*/
|
||||
@Operation(summary = "原料入场记录-准备原生打印数据")
|
||||
@GetMapping(value = "/prepareNativePrint")
|
||||
@RequiresPermissions("xslmes:mes_xsl_raw_material_entry:edit")
|
||||
public Result<Map<String, Object>> prepareNativePrint(@RequestParam(name = "id") String id) {
|
||||
try {
|
||||
MesXslRawMaterialEntry entry = mesXslRawMaterialEntryService.getById(id);
|
||||
if (entry == null) {
|
||||
return Result.error("未找到原料入场记录");
|
||||
}
|
||||
PrintBizTemplateBind bind =
|
||||
printBizTemplateBindService.getByBizCode(MesXslPrintConstants.RAW_MATERIAL_ENTRY_PERM_ID);
|
||||
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(entry);
|
||||
ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping);
|
||||
Map<String, Object> out = new HashMap<>(8);
|
||||
out.put("entryId", entry.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());
|
||||
}
|
||||
}
|
||||
|
||||
@AutoLog(value = "原料入场记录-PDF后端打印")
|
||||
@Operation(summary = "原料入场记录-PDF后端打印")
|
||||
@PostMapping(value = "/printPdf")
|
||||
@RequiresPermissions("xslmes:mes_xsl_raw_material_entry:edit")
|
||||
public Result<String> printPdf(@RequestBody Map<String, Object> 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 不能为空");
|
||||
}
|
||||
MesXslRawMaterialEntry entry = mesXslRawMaterialEntryService.getById(id);
|
||||
if (entry == null) {
|
||||
return Result.error("未找到原料入场记录");
|
||||
}
|
||||
String prefix =
|
||||
StringUtils.isNotBlank(entry.getBarcode()) ? entry.getBarcode() : entry.getId();
|
||||
String fn =
|
||||
StringUtils.isNotBlank(fileName) ? fileName : ("原料入场记录-" + prefix + ".pdf");
|
||||
return printServerPdfJobService.submitPdfBase64(
|
||||
printerName, pdfBase64, fn, "RAW_ENTRY_" + prefix);
|
||||
}
|
||||
|
||||
@RequiresPermissions("xslmes:mes_xsl_raw_material_entry:exportXls")
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, MesXslRawMaterialEntry mesXslRawMaterialEntry) {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.jeecg.modules.print.bootstrap;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.modules.print.service.IPrintBizPermEntityService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 项目启动后异步从数据库 {@code sys_permission} 读取真实 component / component_name,预热
|
||||
* {@code print_biz_perm_entity},不依赖人工白名单操作。关闭:{@code jeecg.print.biz-perm-warmup=false}
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(2000)
|
||||
public class PrintBizPermEntityWarmupRunner implements ApplicationRunner {
|
||||
|
||||
@Autowired private IPrintBizPermEntityService printBizPermEntityService;
|
||||
|
||||
@Value("${jeecg.print.biz-perm-warmup:true}")
|
||||
private boolean warmupEnabled;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
if (!warmupEnabled) {
|
||||
log.info("打印菜单-实体映射预热已关闭(jeecg.print.biz-perm-warmup=false)");
|
||||
return;
|
||||
}
|
||||
CompletableFuture.runAsync(
|
||||
() -> {
|
||||
try {
|
||||
log.info("打印菜单-实体映射:开始异步预热(来源 sys_permission 表)");
|
||||
printBizPermEntityService.warmupMappingsFromSysPermissionTable();
|
||||
} catch (Exception e) {
|
||||
log.warn("打印菜单-实体映射预热失败(不影响系统启动)", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
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<String, PrintBizTypeVO> 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<PrintBizFieldItemVO> 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<PrintBizTypeVO> listAll() {
|
||||
return new ArrayList<>(REGISTRY.values());
|
||||
}
|
||||
|
||||
public static PrintBizTypeVO getByCode(String bizCode) {
|
||||
if (bizCode == null) {
|
||||
return null;
|
||||
}
|
||||
return REGISTRY.get(bizCode.trim());
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ 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.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -20,13 +21,18 @@ 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.IPrintBizBindPermWhitelistService;
|
||||
import org.jeecg.modules.print.service.IPrintBizPermEntityService;
|
||||
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.PrintBizDetailPropertyScanner;
|
||||
import org.jeecg.modules.print.util.PrintBizEntityFieldIntrospector;
|
||||
import org.jeecg.modules.print.util.PrintNativeTemplateFieldExtractor;
|
||||
import org.jeecg.modules.print.vo.PrintBizDetailSlotVO;
|
||||
import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
|
||||
import org.jeecg.modules.print.vo.PrintBizTypeVO;
|
||||
import org.jeecg.modules.print.vo.PrintTemplateFieldItemVO;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -43,6 +49,8 @@ public class PrintBizTemplateBindController extends JeecgController<PrintBizTemp
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
@Autowired private IPrintTemplateService printTemplateService;
|
||||
@Autowired private IPrintBizBindPermWhitelistService printBizBindPermWhitelistService;
|
||||
@Autowired private IPrintBizPermEntityService printBizPermEntityService;
|
||||
|
||||
@Operation(summary = "业务打印绑定-分页列表")
|
||||
@GetMapping("/list")
|
||||
@@ -107,11 +115,41 @@ public class PrintBizTemplateBindController extends JeecgController<PrintBizTemp
|
||||
return Result.OK("删除成功");
|
||||
}
|
||||
|
||||
@Operation(summary = "已注册的业务类型及字段目录")
|
||||
@Operation(summary = "已配置的菜单-实体映射及反射字段(完整目录;biz_code 为菜单 id)")
|
||||
@GetMapping("/bizTypes")
|
||||
@RequiresPermissions("print:bizBind:list")
|
||||
public Result<List<PrintBizTypeVO>> bizTypes() {
|
||||
return Result.OK(PrintBizTypeCatalog.listAll());
|
||||
return Result.OK(printBizPermEntityService.listAllBizTypeVOs());
|
||||
}
|
||||
|
||||
/**
|
||||
* 「新增/编辑业务打印绑定」业务下拉:print_biz_perm_entity 中的映射,并按「打印业务白名单」过滤。
|
||||
* 白名单为空表示不过滤;非空则仅保留菜单 id 在白名单内的项。
|
||||
*/
|
||||
@Operation(summary = "可选业务类型(白名单过滤后)")
|
||||
@GetMapping("/bizTypesForBinding")
|
||||
@RequiresPermissions("print:bizBind:list")
|
||||
public Result<List<PrintBizTypeVO>> bizTypesForBinding() {
|
||||
return Result.OK(printBizBindPermWhitelistService.listBizTypesForBinding());
|
||||
}
|
||||
|
||||
@Operation(summary = "打印业务白名单:当前勾选的菜单 id(catalog 恒为空,避免全库反射超时)")
|
||||
@GetMapping("/permWhitelist")
|
||||
@RequiresPermissions("print:bizBind:whitelist")
|
||||
public Result<PrintBizPermWhitelistVO> getPermWhitelist() {
|
||||
PrintBizPermWhitelistVO vo = new PrintBizPermWhitelistVO();
|
||||
vo.setPermIds(printBizBindPermWhitelistService.listPermIds());
|
||||
vo.setCatalog(Collections.emptyList());
|
||||
return Result.OK(vo);
|
||||
}
|
||||
|
||||
@AutoLog(value = "业务打印绑定-保存打印业务白名单")
|
||||
@Operation(summary = "保存打印业务白名单(按菜单 permission id)")
|
||||
@PostMapping("/permWhitelist")
|
||||
@RequiresPermissions("print:bizBind:whitelist")
|
||||
public Result<String> savePermWhitelist(@RequestBody PermWhitelistBody body) {
|
||||
printBizBindPermWhitelistService.replacePermIds(body == null ? null : body.getPermIds());
|
||||
return Result.OK("保存成功");
|
||||
}
|
||||
|
||||
@Operation(summary = "解析原生模板中的占位字段(bindField)")
|
||||
@@ -131,6 +169,47 @@ public class PrintBizTemplateBindController extends JeecgController<PrintBizTemp
|
||||
return Result.OK(fields);
|
||||
}
|
||||
|
||||
@Operation(summary = "主实体上的明细槽位(List<明细实体> / 明细数组 / 嵌套对象),用于先选明细再反射明细字段")
|
||||
@GetMapping("/detailSlots")
|
||||
@RequiresPermissions("print:bizBind:list")
|
||||
public Result<List<PrintBizDetailSlotVO>> detailSlots(@RequestParam(name = "bizCode") String bizCode) {
|
||||
if (StringUtils.isBlank(bizCode)) {
|
||||
return Result.error("bizCode 不能为空");
|
||||
}
|
||||
PrintBizTypeVO bizVo = printBizPermEntityService.resolveBizTypeVo(bizCode.trim());
|
||||
if (bizVo == null || StringUtils.isBlank(bizVo.getDescription())) {
|
||||
return Result.OK(Collections.emptyList());
|
||||
}
|
||||
Class<?> main = PrintBizEntityFieldIntrospector.tryLoadClass(bizVo.getDescription().trim());
|
||||
if (main == null) {
|
||||
return Result.OK(Collections.emptyList());
|
||||
}
|
||||
return Result.OK(PrintBizDetailPropertyScanner.listSlots(main));
|
||||
}
|
||||
|
||||
@Operation(summary = "反射指定明细槽位元素类的字段(fieldKey 带「属性名.」前缀,用于与模板明细占位绑定)")
|
||||
@GetMapping("/bizFieldsForDetailSlot")
|
||||
@RequiresPermissions("print:bizBind:list")
|
||||
public Result<List<PrintBizFieldItemVO>> bizFieldsForDetailSlot(
|
||||
@RequestParam(name = "bizCode") String bizCode,
|
||||
@RequestParam(name = "detailProperty") String detailProperty,
|
||||
@RequestParam(name = "slotKind", defaultValue = "LIST") String slotKind) {
|
||||
if (StringUtils.isAnyBlank(bizCode, detailProperty)) {
|
||||
return Result.error("bizCode 与 detailProperty 不能为空");
|
||||
}
|
||||
PrintBizTypeVO bizVo = printBizPermEntityService.resolveBizTypeVo(bizCode.trim());
|
||||
if (bizVo == null || StringUtils.isBlank(bizVo.getDescription())) {
|
||||
return Result.OK(Collections.emptyList());
|
||||
}
|
||||
Class<?> main = PrintBizEntityFieldIntrospector.tryLoadClass(bizVo.getDescription().trim());
|
||||
if (main == null) {
|
||||
return Result.OK(Collections.emptyList());
|
||||
}
|
||||
return Result.OK(
|
||||
PrintBizDetailPropertyScanner.listPrefixedDetailFields(
|
||||
main, detailProperty.trim(), slotKind.trim()));
|
||||
}
|
||||
|
||||
@Operation(summary = "按业务编码查询绑定(供打印调用)")
|
||||
@GetMapping("/queryByBizCode")
|
||||
@RequiresPermissions("print:bizBind:list")
|
||||
@@ -183,9 +262,12 @@ public class PrintBizTemplateBindController extends JeecgController<PrintBizTemp
|
||||
return "打印模板不存在";
|
||||
}
|
||||
entity.setTemplateCode(tpl.getTemplateCode());
|
||||
if (PrintBizTypeCatalog.getByCode(entity.getBizCode()) != null
|
||||
&& StringUtils.isBlank(entity.getBizName())) {
|
||||
entity.setBizName(PrintBizTypeCatalog.getByCode(entity.getBizCode()).getBizName());
|
||||
PrintBizTypeVO bizVo = printBizPermEntityService.resolveBizTypeVo(entity.getBizCode());
|
||||
if (bizVo == null) {
|
||||
return "无效的业务:该菜单无法解析到实体类(需菜单 component 为如 xslmes/xxx/XxxList,或在 print_biz_perm_entity 中配置 entity_class;biz_code 为菜单 id)";
|
||||
}
|
||||
if (StringUtils.isBlank(entity.getBizName())) {
|
||||
entity.setBizName(bizVo.getBizName());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -213,4 +295,17 @@ public class PrintBizTemplateBindController extends JeecgController<PrintBizTemp
|
||||
/** 业务对象 JSON(对象或可解析字符串) */
|
||||
private Object bizDataJson;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class PrintBizPermWhitelistVO {
|
||||
/** 已勾选的 sys_permission.id;空集合表示未配置(不限制可选业务) */
|
||||
private java.util.List<String> permIds;
|
||||
/** 完整打印业务目录(含 linkedPermissionId,便于对照菜单) */
|
||||
private java.util.List<PrintBizTypeVO> catalog;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class PermWhitelistBody {
|
||||
private java.util.List<String> permIds;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.jeecg.modules.print.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
|
||||
/** 业务打印绑定可选范围:白名单(sys_permission.id) */
|
||||
@Data
|
||||
@TableName("print_biz_bind_perm_whitelist")
|
||||
public class PrintBizBindPermWhitelist implements Serializable {
|
||||
|
||||
@TableId(type = IdType.INPUT)
|
||||
private String permId;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.jeecg.modules.print.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
|
||||
/** 打印业务:菜单权限与实体类映射(biz_code 使用 perm_id) */
|
||||
@Data
|
||||
@TableName("print_biz_perm_entity")
|
||||
public class PrintBizPermEntity implements Serializable {
|
||||
|
||||
@TableId(value = "perm_id", type = IdType.INPUT)
|
||||
private String permId;
|
||||
|
||||
/**
|
||||
* 实体类全限定名;为空表示占位。ALWAYS 保证 update 时带上 entity_class(含置 null),避免仅主键导致无 SET 子句。
|
||||
*/
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private String entityClass;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.jeecg.modules.print.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.print.entity.PrintBizBindPermWhitelist;
|
||||
|
||||
/** 打印业务菜单白名单 */
|
||||
public interface PrintBizBindPermWhitelistMapper extends BaseMapper<PrintBizBindPermWhitelist> {}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.jeecg.modules.print.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.print.entity.PrintBizPermEntity;
|
||||
|
||||
/** 菜单-实体映射 */
|
||||
public interface PrintBizPermEntityMapper extends BaseMapper<PrintBizPermEntity> {}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.jeecg.modules.print.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import java.util.List;
|
||||
import org.jeecg.modules.print.entity.PrintBizBindPermWhitelist;
|
||||
import org.jeecg.modules.print.vo.PrintBizTypeVO;
|
||||
|
||||
/** 打印业务与系统菜单白名单 */
|
||||
public interface IPrintBizBindPermWhitelistService extends IService<PrintBizBindPermWhitelist> {
|
||||
|
||||
/** 当前白名单中的菜单 id(空集合表示未配置,放行全部目录中的打印业务) */
|
||||
List<String> listPermIds();
|
||||
|
||||
/** 全量替换白名单 */
|
||||
void replacePermIds(List<String> permIds);
|
||||
|
||||
/** 「新增业务打印绑定」下拉里可用的业务类型(print_biz_perm_entity + 反射字段,受白名单过滤) */
|
||||
List<PrintBizTypeVO> listBizTypesForBinding();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.jeecg.modules.print.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import java.util.List;
|
||||
import org.jeecg.modules.print.entity.PrintBizPermEntity;
|
||||
import org.jeecg.modules.print.vo.PrintBizTypeVO;
|
||||
|
||||
/** 菜单与实体映射 + 反射生成业务类型 VO */
|
||||
public interface IPrintBizPermEntityService extends IService<PrintBizPermEntity> {
|
||||
|
||||
PrintBizPermEntity getByPermId(String permId);
|
||||
|
||||
/** 全部映射对应的业务类型(含反射字段);用于目录展示与白名单弹窗 catalog */
|
||||
List<PrintBizTypeVO> listAllBizTypeVOs();
|
||||
|
||||
/**
|
||||
* 按白名单过滤:permIds 为空表示不过滤(返回全部映射);非空则仅保留 perm_id 在集合内的项。
|
||||
*/
|
||||
List<PrintBizTypeVO> listBizTypeVOsFiltered(List<String> whitelistPermIds);
|
||||
|
||||
/**
|
||||
* 解析单个菜单对应的业务类型:优先 print_biz_perm_entity,否则按菜单 component 推断实体类并反射字段。
|
||||
*/
|
||||
PrintBizTypeVO resolveBizTypeVo(String permId);
|
||||
|
||||
/**
|
||||
* 将白名单勾选的菜单写入/补全 print_biz_perm_entity:已有可加载的 entity_class 不覆盖;否则按菜单 component 推断并插入或更新。
|
||||
*/
|
||||
void upsertMappingsForWhitelist(List<String> permIds);
|
||||
|
||||
/**
|
||||
* 启动预热:扫描数据库 sys_permission(menu_type=1 且 component 非空),用表中真实的 component / component_name 推断实体类并写入
|
||||
* print_biz_perm_entity(不覆盖已有可加载的 entity_class)。
|
||||
*/
|
||||
void warmupMappingsFromSysPermissionTable();
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.jeecg.modules.print.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.modules.print.entity.PrintBizBindPermWhitelist;
|
||||
import org.jeecg.modules.print.mapper.PrintBizBindPermWhitelistMapper;
|
||||
import org.jeecg.modules.print.service.IPrintBizBindPermWhitelistService;
|
||||
import org.jeecg.modules.print.service.IPrintBizPermEntityService;
|
||||
import org.jeecg.modules.print.vo.PrintBizTypeVO;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class PrintBizBindPermWhitelistServiceImpl
|
||||
extends ServiceImpl<PrintBizBindPermWhitelistMapper, PrintBizBindPermWhitelist>
|
||||
implements IPrintBizBindPermWhitelistService {
|
||||
|
||||
@Autowired private IPrintBizPermEntityService printBizPermEntityService;
|
||||
|
||||
@Override
|
||||
public List<String> listPermIds() {
|
||||
return list().stream().map(PrintBizBindPermWhitelist::getPermId).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void replacePermIds(List<String> permIds) {
|
||||
remove(
|
||||
Wrappers.<PrintBizBindPermWhitelist>lambdaQuery()
|
||||
.isNotNull(PrintBizBindPermWhitelist::getPermId));
|
||||
if (permIds == null || permIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<String> seen = new HashSet<>();
|
||||
List<PrintBizBindPermWhitelist> batch = new ArrayList<>();
|
||||
for (String raw : permIds) {
|
||||
String id = StringUtils.trimToEmpty(raw);
|
||||
if (id.isEmpty() || seen.contains(id)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(id);
|
||||
PrintBizBindPermWhitelist row = new PrintBizBindPermWhitelist();
|
||||
row.setPermId(id);
|
||||
batch.add(row);
|
||||
}
|
||||
if (!batch.isEmpty()) {
|
||||
// 分段写入,避免单次 SQL 包过大
|
||||
saveBatch(batch, 500);
|
||||
List<String> savedIds =
|
||||
batch.stream().map(PrintBizBindPermWhitelist::getPermId).collect(Collectors.toList());
|
||||
printBizPermEntityService.upsertMappingsForWhitelist(savedIds);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PrintBizTypeVO> listBizTypesForBinding() {
|
||||
List<String> whitelist = listPermIds();
|
||||
return printBizPermEntityService.listBizTypeVOsFiltered(whitelist);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package org.jeecg.modules.print.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.modules.print.entity.PrintBizPermEntity;
|
||||
import org.jeecg.modules.print.mapper.PrintBizPermEntityMapper;
|
||||
import org.jeecg.modules.print.service.IPrintBizPermEntityService;
|
||||
import org.jeecg.modules.print.util.PrintBizEntityFieldIntrospector;
|
||||
import org.jeecg.modules.print.util.PrintBizMenuEntityInference;
|
||||
import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
|
||||
import org.jeecg.modules.print.vo.PrintBizTypeVO;
|
||||
import org.jeecg.modules.system.entity.SysPermission;
|
||||
import org.jeecg.modules.system.service.ISysPermissionService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class PrintBizPermEntityServiceImpl
|
||||
extends ServiceImpl<PrintBizPermEntityMapper, PrintBizPermEntity>
|
||||
implements IPrintBizPermEntityService {
|
||||
|
||||
@Autowired private ISysPermissionService sysPermissionService;
|
||||
|
||||
@Override
|
||||
public PrintBizPermEntity getByPermId(String permId) {
|
||||
if (StringUtils.isBlank(permId)) {
|
||||
return null;
|
||||
}
|
||||
return getById(permId.trim());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PrintBizTypeVO> listAllBizTypeVOs() {
|
||||
List<PrintBizTypeVO> out = new ArrayList<>();
|
||||
// 仅使用 print_biz_perm_entity,避免全库扫菜单+反射导致接口超时;由「保存白名单」时 upsert 写入表
|
||||
for (PrintBizPermEntity row : list()) {
|
||||
if (row == null || StringUtils.isBlank(row.getPermId())) {
|
||||
continue;
|
||||
}
|
||||
PrintBizTypeVO vo = buildVoForPermId(row.getPermId());
|
||||
if (vo != null) {
|
||||
out.add(vo);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PrintBizTypeVO> listBizTypeVOsFiltered(List<String> whitelistPermIds) {
|
||||
if (whitelistPermIds == null || whitelistPermIds.isEmpty()) {
|
||||
return listAllBizTypeVOs();
|
||||
}
|
||||
List<PrintBizTypeVO> out = new ArrayList<>();
|
||||
for (String raw : whitelistPermIds) {
|
||||
String id = StringUtils.trimToEmpty(raw);
|
||||
if (id.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
PrintBizTypeVO vo = buildVoForPermId(id);
|
||||
if (vo != null) {
|
||||
out.add(vo);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintBizTypeVO resolveBizTypeVo(String permId) {
|
||||
return buildVoForPermId(permId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void upsertMappingsForWhitelist(List<String> permIds) {
|
||||
if (permIds == null || permIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<String> ids = new ArrayList<>();
|
||||
Set<String> seen = new HashSet<>();
|
||||
for (String raw : permIds) {
|
||||
String id = StringUtils.trimToEmpty(raw);
|
||||
if (id.isEmpty() || seen.contains(id)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(id);
|
||||
ids.add(id);
|
||||
}
|
||||
if (ids.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// 批量查询,避免勾选数百条时 N 次 getById 导致超时
|
||||
Map<String, PrintBizPermEntity> existingMap = new HashMap<>(ids.size());
|
||||
for (PrintBizPermEntity e : listByIds(ids)) {
|
||||
if (e != null && StringUtils.isNotBlank(e.getPermId())) {
|
||||
existingMap.put(e.getPermId(), e);
|
||||
}
|
||||
}
|
||||
Map<String, SysPermission> permMap = new HashMap<>(ids.size());
|
||||
for (SysPermission p : sysPermissionService.listByIds(ids)) {
|
||||
if (p != null && StringUtils.isNotBlank(p.getId())) {
|
||||
permMap.put(p.getId(), p);
|
||||
}
|
||||
}
|
||||
List<PrintBizPermEntity> toSave = new ArrayList<>();
|
||||
for (String id : ids) {
|
||||
PrintBizPermEntity existing = existingMap.get(id);
|
||||
// 已有可加载的实体类配置则保留,不再覆盖
|
||||
if (existing != null && StringUtils.isNotBlank(existing.getEntityClass())) {
|
||||
Class<?> loaded =
|
||||
PrintBizEntityFieldIntrospector.tryLoadClass(existing.getEntityClass().trim());
|
||||
if (loaded != null) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
SysPermission p = permMap.get(id);
|
||||
String inferred = PrintBizMenuEntityInference.inferEntityClassFqn(p);
|
||||
PrintBizPermEntity row = existing != null ? existing : new PrintBizPermEntity();
|
||||
row.setPermId(id);
|
||||
if (StringUtils.isNotBlank(inferred)
|
||||
&& PrintBizEntityFieldIntrospector.tryLoadClass(inferred) != null) {
|
||||
row.setEntityClass(inferred);
|
||||
} else {
|
||||
// 勾选即落库(与勾选数量一致);无法推断或类不在 classpath 时占位 NULL,可手工 UPDATE
|
||||
row.setEntityClass(null);
|
||||
}
|
||||
toSave.add(row);
|
||||
}
|
||||
if (!toSave.isEmpty()) {
|
||||
saveOrUpdateBatch(toSave, 500);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warmupMappingsFromSysPermissionTable() {
|
||||
List<SysPermission> menus =
|
||||
sysPermissionService
|
||||
.lambdaQuery()
|
||||
.eq(SysPermission::getMenuType, 1)
|
||||
.isNotNull(SysPermission::getComponent)
|
||||
.list();
|
||||
if (menus == null || menus.isEmpty()) {
|
||||
log.info("打印菜单-实体映射预热:无子菜单数据");
|
||||
return;
|
||||
}
|
||||
List<String> ids =
|
||||
menus.stream()
|
||||
.map(SysPermission::getId)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
Map<String, PrintBizPermEntity> existingMap = new HashMap<>(ids.size());
|
||||
for (PrintBizPermEntity e : listByIds(ids)) {
|
||||
if (e != null && StringUtils.isNotBlank(e.getPermId())) {
|
||||
existingMap.put(e.getPermId(), e);
|
||||
}
|
||||
}
|
||||
List<PrintBizPermEntity> toSave = new ArrayList<>();
|
||||
for (SysPermission p : menus) {
|
||||
if (p == null || StringUtils.isBlank(p.getId())) {
|
||||
continue;
|
||||
}
|
||||
String comp = p.getComponent();
|
||||
if (StringUtils.isBlank(comp)
|
||||
|| comp.contains("layouts")
|
||||
|| comp.contains("RouteView")
|
||||
|| comp.contains("ParentView")) {
|
||||
continue;
|
||||
}
|
||||
PrintBizPermEntity existing = existingMap.get(p.getId());
|
||||
if (existing != null && StringUtils.isNotBlank(existing.getEntityClass())) {
|
||||
if (PrintBizEntityFieldIntrospector.tryLoadClass(existing.getEntityClass().trim()) != null) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
String inferred = PrintBizMenuEntityInference.inferEntityClassFqn(p);
|
||||
if (StringUtils.isBlank(inferred)
|
||||
|| PrintBizEntityFieldIntrospector.tryLoadClass(inferred) == null) {
|
||||
continue;
|
||||
}
|
||||
PrintBizPermEntity row = existing != null ? existing : new PrintBizPermEntity();
|
||||
row.setPermId(p.getId());
|
||||
row.setEntityClass(inferred);
|
||||
toSave.add(row);
|
||||
}
|
||||
if (toSave.isEmpty()) {
|
||||
log.info("打印菜单-实体映射预热:无新增可解析项");
|
||||
return;
|
||||
}
|
||||
saveOrUpdateBatch(toSave, 500);
|
||||
log.info("打印菜单-实体映射预热完成,本次写入/更新 {} 条(数据来自 sys_permission 表中的 component/component_name)", toSave.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 显式表优先;否则按 SysPermission.component 推断实体类名并加载字段。
|
||||
*/
|
||||
private PrintBizTypeVO buildVoForPermId(String permId) {
|
||||
if (StringUtils.isBlank(permId)) {
|
||||
return null;
|
||||
}
|
||||
PrintBizPermEntity row = getById(permId.trim());
|
||||
String entityFqn = null;
|
||||
if (row != null && StringUtils.isNotBlank(row.getEntityClass())) {
|
||||
entityFqn = row.getEntityClass().trim();
|
||||
} else {
|
||||
SysPermission p = sysPermissionService.getById(permId.trim());
|
||||
entityFqn = PrintBizMenuEntityInference.inferEntityClassFqn(p);
|
||||
}
|
||||
if (StringUtils.isBlank(entityFqn)) {
|
||||
return null;
|
||||
}
|
||||
Class<?> clazz = PrintBizEntityFieldIntrospector.tryLoadClass(entityFqn);
|
||||
List<PrintBizFieldItemVO> fields =
|
||||
clazz == null ? new ArrayList<>() : PrintBizEntityFieldIntrospector.listFields(clazz);
|
||||
if (clazz == null) {
|
||||
// 类不在 classpath(模块未引入)时不生成下拉项,避免空字段误导
|
||||
return null;
|
||||
}
|
||||
PrintBizTypeVO vo = new PrintBizTypeVO();
|
||||
vo.setBizCode(permId.trim());
|
||||
vo.setLinkedPermissionId(permId.trim());
|
||||
vo.setBizName(resolveMenuName(permId.trim()));
|
||||
vo.setDescription(entityFqn);
|
||||
vo.setFields(fields);
|
||||
return vo;
|
||||
}
|
||||
|
||||
private String resolveMenuName(String permId) {
|
||||
SysPermission p = sysPermissionService.getById(permId);
|
||||
if (p != null && StringUtils.isNotBlank(p.getName())) {
|
||||
return p.getName();
|
||||
}
|
||||
return permId;
|
||||
}
|
||||
}
|
||||
@@ -43,11 +43,33 @@ public final class PrintBizDataMappingUtil {
|
||||
if (cur == null || p.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
cur = cur.get(p);
|
||||
if (cur.isArray()) {
|
||||
if (isNonNegativeIntString(p)) {
|
||||
cur = cur.get(Integer.parseInt(p));
|
||||
} else {
|
||||
JsonNode first = cur.size() > 0 ? cur.get(0) : null;
|
||||
cur = first != null ? first.get(p) : null;
|
||||
}
|
||||
} else {
|
||||
cur = cur.get(p);
|
||||
}
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
/** 全数字段按数组下标解析,否则在 JSON 数组上取首行再取属性(如 lines.qty 表示第一行明细的 qty) */
|
||||
private static boolean isNonNegativeIntString(String p) {
|
||||
if (p == null || p.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < p.length(); i++) {
|
||||
if (!Character.isDigit(p.charAt(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void setPath(ObjectNode target, String path, JsonNode value) {
|
||||
if (target == null || StringUtils.isBlank(path)) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
package org.jeecg.modules.print.util;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.jeecg.modules.print.vo.PrintBizDetailSlotVO;
|
||||
import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
|
||||
|
||||
/**
|
||||
* 扫描主实体类上可作为「明细」的来源属性:{@code List<Entity>} / {@code Set<Entity>} / 数组 / 嵌套业务对象。
|
||||
*/
|
||||
public final class PrintBizDetailPropertyScanner {
|
||||
|
||||
private PrintBizDetailPropertyScanner() {}
|
||||
|
||||
/**
|
||||
* 根据已选明细槽位解析元素类型:LIST 取集合元素类或数组组件类型;OBJECT 取嵌套属性类型。
|
||||
*
|
||||
* @param slotKind LIST 或 OBJECT(与 {@link PrintBizDetailSlotVO#getSlotKind()} 一致)
|
||||
*/
|
||||
public static Class<?> resolveItemClassForSlot(
|
||||
Class<?> mainClazz, String propertyName, String slotKind) {
|
||||
if (mainClazz == null || StringUtils.isBlank(propertyName)) {
|
||||
return null;
|
||||
}
|
||||
Field f = findDeclaredField(mainClazz, propertyName.trim());
|
||||
if (f == null) {
|
||||
return null;
|
||||
}
|
||||
if ("OBJECT".equalsIgnoreCase(StringUtils.trimToEmpty(slotKind))) {
|
||||
Class<?> t = f.getType();
|
||||
return isLikelyBizBean(t) ? t : null;
|
||||
}
|
||||
Class<?> elem = resolveCollectionElementClass(f);
|
||||
if (elem != null) {
|
||||
return elem;
|
||||
}
|
||||
if (f.getType().isArray()) {
|
||||
Class<?> comp = f.getType().getComponentType();
|
||||
return isSimpleOrJdkValueType(comp) ? null : comp;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Field findDeclaredField(Class<?> start, String name) {
|
||||
Class<?> c = start;
|
||||
while (c != null && c != Object.class) {
|
||||
try {
|
||||
return c.getDeclaredField(name);
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
c = c.getSuperclass();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 明细元素类型反射字段,fieldKey 已带「属性名.」前缀,便于与模板明细占位映射 */
|
||||
public static List<PrintBizFieldItemVO> listPrefixedDetailFields(
|
||||
Class<?> mainClazz, String propertyName, String slotKind) {
|
||||
Class<?> item = resolveItemClassForSlot(mainClazz, propertyName, slotKind);
|
||||
if (item == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<PrintBizFieldItemVO> raw = PrintBizEntityFieldIntrospector.listFields(item);
|
||||
String prefix = propertyName.trim();
|
||||
List<PrintBizFieldItemVO> out = new ArrayList<>(raw.size());
|
||||
for (PrintBizFieldItemVO x : raw) {
|
||||
String path = prefix + "." + x.getFieldKey();
|
||||
String label = "明细「" + prefix + "」→ " + x.getLabel();
|
||||
out.add(new PrintBizFieldItemVO(path, label, x.getDescription()));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public static List<PrintBizDetailSlotVO> listSlots(Class<?> mainClazz) {
|
||||
Map<String, PrintBizDetailSlotVO> ordered = new LinkedHashMap<>();
|
||||
Class<?> c = mainClazz;
|
||||
while (c != null && c != Object.class) {
|
||||
for (Field f : c.getDeclaredFields()) {
|
||||
int mod = f.getModifiers();
|
||||
if (Modifier.isStatic(mod) || f.isSynthetic()) {
|
||||
continue;
|
||||
}
|
||||
String name = f.getName();
|
||||
if ("serialVersionUID".equals(name)) {
|
||||
continue;
|
||||
}
|
||||
Class<?> elem = resolveCollectionElementClass(f);
|
||||
if (elem != null && isLikelyBizBean(elem)) {
|
||||
ordered.putIfAbsent(
|
||||
name,
|
||||
new PrintBizDetailSlotVO(name, elem.getName(), "LIST", resolveFieldLabel(f)));
|
||||
continue;
|
||||
}
|
||||
Class<?> ft = f.getType();
|
||||
if (ft.isArray() && !isSimpleOrJdkValueType(ft.getComponentType())) {
|
||||
Class<?> comp = ft.getComponentType();
|
||||
ordered.putIfAbsent(
|
||||
name, new PrintBizDetailSlotVO(name, comp.getName(), "LIST", resolveFieldLabel(f)));
|
||||
continue;
|
||||
}
|
||||
if (!ft.isPrimitive()
|
||||
&& !ft.getName().startsWith("java.lang")
|
||||
&& !Number.class.isAssignableFrom(ft)
|
||||
&& !java.util.Date.class.isAssignableFrom(ft)
|
||||
&& !ft.getName().startsWith("java.time")
|
||||
&& !Map.class.isAssignableFrom(ft)
|
||||
&& !Collection.class.isAssignableFrom(ft)
|
||||
&& isLikelyBizBean(ft)) {
|
||||
ordered.putIfAbsent(
|
||||
name, new PrintBizDetailSlotVO(name, ft.getName(), "OBJECT", resolveFieldLabel(f)));
|
||||
}
|
||||
}
|
||||
c = c.getSuperclass();
|
||||
}
|
||||
return new ArrayList<>(ordered.values());
|
||||
}
|
||||
|
||||
/** 解析 List<T> / Set<T> 的元素类型 T */
|
||||
private static Class<?> resolveCollectionElementClass(Field f) {
|
||||
Type gt = f.getGenericType();
|
||||
if (!(gt instanceof ParameterizedType)) {
|
||||
return null;
|
||||
}
|
||||
ParameterizedType pt = (ParameterizedType) gt;
|
||||
Type raw = pt.getRawType();
|
||||
if (!(raw instanceof Class<?> rc) || !Collection.class.isAssignableFrom(rc)) {
|
||||
return null;
|
||||
}
|
||||
Type[] args = pt.getActualTypeArguments();
|
||||
if (args.length != 1) {
|
||||
return null;
|
||||
}
|
||||
Type arg0 = args[0];
|
||||
if (arg0 instanceof Class<?>) {
|
||||
Class<?> ac = (Class<?>) arg0;
|
||||
return isSimpleOrJdkValueType(ac) ? null : ac;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isSimpleOrJdkValueType(Class<?> cl) {
|
||||
if (cl == null || cl.isPrimitive()) {
|
||||
return true;
|
||||
}
|
||||
if (cl.isEnum()) {
|
||||
return true;
|
||||
}
|
||||
String n = cl.getName();
|
||||
if (n.startsWith("java.lang") || n.startsWith("java.time")) {
|
||||
return true;
|
||||
}
|
||||
if (Number.class.isAssignableFrom(cl) || java.util.Date.class.isAssignableFrom(cl)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 非 JDK 简单值、非集合/Map 的自定义类型视为可映射明细实体 */
|
||||
private static boolean isLikelyBizBean(Class<?> cl) {
|
||||
return cl != null && !cl.isEnum() && !isSimpleOrJdkValueType(cl) && !Map.class.isAssignableFrom(cl);
|
||||
}
|
||||
|
||||
private static String resolveFieldLabel(Field f) {
|
||||
Schema schema = f.getAnnotation(Schema.class);
|
||||
if (schema != null && StringUtils.isNotBlank(schema.description())) {
|
||||
return schema.description().trim();
|
||||
}
|
||||
Excel excel = f.getAnnotation(Excel.class);
|
||||
if (excel != null && StringUtils.isNotBlank(excel.name())) {
|
||||
return excel.name().trim();
|
||||
}
|
||||
return f.getName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.jeecg.modules.print.util;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
|
||||
/**
|
||||
* 从实体类反射可映射字段(供打印业务绑定下拉与映射);子类字段优先于父类同名字段。
|
||||
*/
|
||||
public final class PrintBizEntityFieldIntrospector {
|
||||
|
||||
private PrintBizEntityFieldIntrospector() {}
|
||||
|
||||
public static List<PrintBizFieldItemVO> listFields(Class<?> clazz) {
|
||||
Map<String, PrintBizFieldItemVO> ordered = new LinkedHashMap<>();
|
||||
Class<?> c = clazz;
|
||||
while (c != null && c != Object.class) {
|
||||
for (Field f : c.getDeclaredFields()) {
|
||||
int mod = f.getModifiers();
|
||||
if (Modifier.isStatic(mod) || f.isSynthetic()) {
|
||||
continue;
|
||||
}
|
||||
String name = f.getName();
|
||||
if ("serialVersionUID".equals(name)) {
|
||||
continue;
|
||||
}
|
||||
ordered.putIfAbsent(name, new PrintBizFieldItemVO(name, resolveLabel(f), ""));
|
||||
}
|
||||
c = c.getSuperclass();
|
||||
}
|
||||
return new ArrayList<>(ordered.values());
|
||||
}
|
||||
|
||||
private static String resolveLabel(Field f) {
|
||||
Schema schema = f.getAnnotation(Schema.class);
|
||||
if (schema != null && StringUtils.isNotBlank(schema.description())) {
|
||||
return schema.description().trim();
|
||||
}
|
||||
Excel excel = f.getAnnotation(Excel.class);
|
||||
if (excel != null && StringUtils.isNotBlank(excel.name())) {
|
||||
return excel.name().trim();
|
||||
}
|
||||
return f.getName();
|
||||
}
|
||||
|
||||
/** 按全限定类名加载 Class,失败返回 null */
|
||||
public static Class<?> tryLoadClass(String entityClassFqn) {
|
||||
if (StringUtils.isBlank(entityClassFqn)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Class.forName(entityClassFqn.trim());
|
||||
} catch (Throwable e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package org.jeecg.modules.print.util;
|
||||
|
||||
import java.util.Objects;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.modules.system.entity.SysPermission;
|
||||
|
||||
/**
|
||||
* 根据菜单 component / componentName 推断实体类全名。<br>
|
||||
* 典型:<br>
|
||||
* - xslmes/mesXslWarehouseArea/MesXslWarehouseAreaList<br>
|
||||
* - mes/materialinfo/index + componentName=MesMaterialList → org.jeecg.modules.mes.material.entity.MesMaterial
|
||||
*/
|
||||
public final class PrintBizMenuEntityInference {
|
||||
|
||||
/** MES 物料等菜单实体默认落在该包下(与工程 mes.material.entity 一致) */
|
||||
private static final String MES_MATERIAL_ENTITY_PKG = "org.jeecg.modules.mes.material.entity.";
|
||||
|
||||
private PrintBizMenuEntityInference() {}
|
||||
|
||||
/**
|
||||
* @return 实体类全限定名;无法推断时返回 null(按钮、目录等)
|
||||
*/
|
||||
public static String inferEntityClassFqn(SysPermission permission) {
|
||||
if (permission == null) {
|
||||
return null;
|
||||
}
|
||||
if (Objects.equals(permission.getMenuType(), 2)) {
|
||||
return null;
|
||||
}
|
||||
String comp = permission.getComponent();
|
||||
if (StringUtils.isBlank(comp)) {
|
||||
return null;
|
||||
}
|
||||
String c = comp.trim();
|
||||
if (c.contains("layouts")
|
||||
|| c.contains("RouteView")
|
||||
|| c.contains("ParentView")
|
||||
|| c.startsWith("http")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Jeecg Vue:component 常为 mes/xxx/index,实体类名在 component_name(如 MesMaterialList)
|
||||
if (c.startsWith("mes/") && c.endsWith("/index")) {
|
||||
String fromMesIndex =
|
||||
tryInferMesMaterialModuleFromComponentName(permission.getComponentName());
|
||||
if (StringUtils.isNotBlank(fromMesIndex)) {
|
||||
return fromMesIndex;
|
||||
}
|
||||
}
|
||||
|
||||
String[] segs = c.split("/");
|
||||
if (segs.length < 2) {
|
||||
return null;
|
||||
}
|
||||
String last = segs[segs.length - 1];
|
||||
if (StringUtils.isBlank(last)) {
|
||||
return null;
|
||||
}
|
||||
String simple =
|
||||
last.endsWith("List") ? last.substring(0, last.length() - 4) : last;
|
||||
if (simple.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String module = segs[0];
|
||||
if (StringUtils.isBlank(module)) {
|
||||
return null;
|
||||
}
|
||||
String trial = "org.jeecg.modules." + module + ".entity." + simple;
|
||||
if (PrintBizEntityFieldIntrospector.tryLoadClass(trial) != null) {
|
||||
return trial;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** mes 模块下 index 路由:用 componentName(MesXxxList)推断 mes.material.entity.MesXxx */
|
||||
private static String tryInferMesMaterialModuleFromComponentName(String componentName) {
|
||||
if (StringUtils.isBlank(componentName)) {
|
||||
return null;
|
||||
}
|
||||
String cn = componentName.trim();
|
||||
if (!cn.endsWith("List")) {
|
||||
return null;
|
||||
}
|
||||
String simple = cn.substring(0, cn.length() - 4);
|
||||
if (simple.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String trial = MES_MATERIAL_ENTITY_PKG + simple;
|
||||
if (PrintBizEntityFieldIntrospector.tryLoadClass(trial) != null) {
|
||||
return trial;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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 PrintBizDetailSlotVO implements Serializable {
|
||||
|
||||
@Schema(description = "Java 属性名(JSON 路径前缀,如 lines、headerExt)")
|
||||
private String propertyName;
|
||||
|
||||
@Schema(description = "明细元素类型全限定名")
|
||||
private String itemEntityClassFqn;
|
||||
|
||||
@Schema(description = "LIST=集合明细一行映射;OBJECT=嵌套对象字段映射")
|
||||
private String slotKind;
|
||||
|
||||
@Schema(description = "展示名")
|
||||
private String label;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import lombok.Data;
|
||||
@Data
|
||||
@Schema(description = "可配置的业务类型")
|
||||
public class PrintBizTypeVO implements Serializable {
|
||||
@Schema(description = "业务编码")
|
||||
@Schema(description = "业务编码(与菜单权限 id、绑定表 biz_code 一致)")
|
||||
private String bizCode;
|
||||
|
||||
@Schema(description = "业务名称")
|
||||
@@ -17,6 +17,13 @@ public class PrintBizTypeVO implements Serializable {
|
||||
@Schema(description = "说明")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 与「系统菜单」中的菜单主键(sys_permission.id)对应;用于「打印业务白名单」按菜单勾选。
|
||||
* 未设置表示未挂菜单,白名单生效时仍可出现于下拉(避免仅后端注册、尚未配菜单的业务被误过滤)。
|
||||
*/
|
||||
@Schema(description = "关联菜单权限ID(sys_permission.id)")
|
||||
private String linkedPermissionId;
|
||||
|
||||
@Schema(description = "业务侧可用字段目录")
|
||||
private List<PrintBizFieldItemVO> fields;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
-- 打印业务可选范围:白名单(勾选 sys_permission.id,对应 PrintBizTypeCatalog 中 linkedPermissionId)
|
||||
CREATE TABLE IF NOT EXISTS `print_biz_bind_perm_whitelist` (
|
||||
`perm_id` varchar(36) NOT NULL COMMENT 'sys_permission 主键(菜单/功能)',
|
||||
PRIMARY KEY (`perm_id`)
|
||||
) 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 '1900000000000000125', '1900000000000000120', '打印业务白名单', NULL, NULL, 0, NULL, NULL, 2, 'print:bizBind:whitelist', '1', 5.00, 0, NULL, 1, 0, 0, 0, '配置哪些菜单关联的打印业务可出现在「新增业务打印绑定」中', 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000125');
|
||||
@@ -0,0 +1,17 @@
|
||||
-- 菜单权限与打印业务实体类映射(biz_code / 绑定表业务编码 = perm_id)
|
||||
CREATE TABLE IF NOT EXISTS `print_biz_perm_entity` (
|
||||
`perm_id` varchar(36) NOT NULL COMMENT 'sys_permission.id',
|
||||
`entity_class` varchar(512) NOT NULL COMMENT '实体类全限定名(用于反射字段)',
|
||||
PRIMARY KEY (`perm_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='打印业务-菜单与实体映射';
|
||||
|
||||
-- 原材料卡片菜单 -> MesXslRawMaterialCard
|
||||
INSERT INTO `print_biz_perm_entity` (`perm_id`, `entity_class`)
|
||||
SELECT '1900000000000000540', 'org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `print_biz_perm_entity` WHERE `perm_id` = '1900000000000000540');
|
||||
|
||||
-- 历史绑定:业务编码由语义码改为菜单 id(与白名单、映射表一致)
|
||||
UPDATE `print_biz_template_bind`
|
||||
SET `biz_code` = '1900000000000000540'
|
||||
WHERE `biz_code` = 'MES_RAW_MATERIAL_CARD';
|
||||
@@ -0,0 +1,3 @@
|
||||
-- 允许 entity_class 为空:白名单勾选的菜单优先落库占位,无法按 component 推断时再手工补全
|
||||
ALTER TABLE `print_biz_perm_entity`
|
||||
MODIFY COLUMN `entity_class` varchar(512) NULL COMMENT '实体类全限定名;为空表示仅勾选占位,需手工配置或菜单不符合推断规则';
|
||||
@@ -6,8 +6,12 @@ enum Api {
|
||||
edit = '/print/bizTemplateBind/edit',
|
||||
deleteOne = '/print/bizTemplateBind/delete',
|
||||
bizTypes = '/print/bizTemplateBind/bizTypes',
|
||||
bizTypesForBinding = '/print/bizTemplateBind/bizTypesForBinding',
|
||||
permWhitelist = '/print/bizTemplateBind/permWhitelist',
|
||||
parseTemplateFields = '/print/bizTemplateBind/parseTemplateFields',
|
||||
previewMappedData = '/print/bizTemplateBind/previewMappedData',
|
||||
detailSlots = '/print/bizTemplateBind/detailSlots',
|
||||
bizFieldsForDetailSlot = '/print/bizTemplateBind/bizFieldsForDetailSlot',
|
||||
}
|
||||
|
||||
export const list = (params) => defHttp.get({ url: Api.list, params });
|
||||
@@ -18,6 +22,13 @@ 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 bizTypesForBinding = () => defHttp.get({ url: Api.bizTypesForBinding });
|
||||
/** 白名单:已勾选菜单 id + 完整业务目录 */
|
||||
export const getPermWhitelist = () => defHttp.get({ url: Api.permWhitelist });
|
||||
/** 勾选菜单多时后端需批量 upsert,默认 10s 易超时 */
|
||||
export const savePermWhitelist = (data: { permIds: string[] }) =>
|
||||
defHttp.post({ url: Api.permWhitelist, data, timeout: 3 * 60 * 1000 });
|
||||
export const parseTemplateFields = (templateId: string) =>
|
||||
defHttp.get({
|
||||
url: Api.parseTemplateFields,
|
||||
@@ -27,3 +38,21 @@ export const parseTemplateFields = (templateId: string) =>
|
||||
/** 预览映射后的打印数据 */
|
||||
export const previewMappedData = (data: { bizCode: string; bizDataJson: Record<string, unknown> }) =>
|
||||
defHttp.post({ url: Api.previewMappedData, data });
|
||||
|
||||
/** 主实体上可作为明细的数据属性(List/数组/嵌套对象) */
|
||||
export const detailSlots = (bizCode: string) =>
|
||||
defHttp.get<{ propertyName: string; itemEntityClassFqn: string; slotKind: string; label: string }[]>({
|
||||
url: Api.detailSlots,
|
||||
params: { bizCode },
|
||||
});
|
||||
|
||||
/** 反射明细元素类字段,fieldKey 已带「属性名.」前缀 */
|
||||
export const bizFieldsForDetailSlot = (params: {
|
||||
bizCode: string;
|
||||
detailProperty: string;
|
||||
slotKind?: string;
|
||||
}) =>
|
||||
defHttp.get<{ fieldKey: string; label: string; description?: string }[]>({
|
||||
url: Api.bizFieldsForDetailSlot,
|
||||
params,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
<div>
|
||||
<BasicTable @register="registerTable">
|
||||
<template #tableTitle>
|
||||
<a-button type="primary" @click="openCreate" v-auth="'print:bizBind:add'">新增绑定</a-button>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="openCreate" v-auth="'print:bizBind:add'">新增绑定</a-button>
|
||||
<a-button @click="openPrintBizWhitelist" :loading="whitelistLoading" v-auth="'print:bizBind:whitelist'">
|
||||
打印业务白名单
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<TableAction
|
||||
@@ -29,99 +34,235 @@
|
||||
<BasicModal
|
||||
@register="registerModal"
|
||||
:title="modalTitle"
|
||||
width="920px"
|
||||
width="1000px"
|
||||
@ok="submitModal"
|
||||
:confirm-loading="modalSubmitLoading"
|
||||
destroy-on-close
|
||||
wrap-class-name="biz-bind-modal-wrap"
|
||||
>
|
||||
<a-spin :spinning="tplLoading || parseLoading">
|
||||
<a-space direction="vertical" style="width: 100%" size="middle">
|
||||
<a-spin :spinning="modalDataLoading || parseLoading">
|
||||
<div class="biz-bind-form">
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
message="配置步骤"
|
||||
description="1)选择业务类型;2)选择已发布的打印模板;3)为模板每个占位字段(bindField)指定对应的业务 JSON 字段;可点击「同名匹配」快速对齐。"
|
||||
class="bind-alert"
|
||||
message="配置说明"
|
||||
description="按卡片顺序操作:先选业务与模板 → 若模板含明细占位,在「明细数据来源」中选择主实体上的集合/嵌套对象 → 点击「解析模板占位字段」→ 在下方「主表参数」「明细与表格」中分别为每个占位选择业务字段。主表参数一般映射主实体字段;明细占位可选带「明细前缀」的路径(如 lines.qty)。支持 lines.qty(首行)或 lines.0.qty。"
|
||||
/>
|
||||
|
||||
<a-form layout="vertical">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="业务" required>
|
||||
<a-select
|
||||
v-model:value="form.bizCode"
|
||||
:options="bizSelectOptions"
|
||||
placeholder="选择业务"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
:disabled="isEditMode"
|
||||
@change="onBizCodeChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="打印模板" required>
|
||||
<a-select
|
||||
v-model:value="form.templateId"
|
||||
:options="tplSelectOptions"
|
||||
placeholder="选择模板"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
@change="onTemplateChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="备注">
|
||||
<a-input v-model:value="form.remark" placeholder="可选" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-card title="基础信息" size="small" :bordered="true" class="bind-card">
|
||||
<a-form layout="vertical" class="bind-card-form">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="业务"
|
||||
required
|
||||
extra="业务编码为菜单 id;后端按 print_biz_perm_entity 或菜单 component 推断主实体并反射主表字段。"
|
||||
>
|
||||
<a-select
|
||||
v-model:value="form.bizCode"
|
||||
:options="bizSelectOptions"
|
||||
placeholder="选择业务"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
:disabled="isEditMode"
|
||||
@change="onBizCodeChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="打印模板" required extra="请选择已发布的原生打印模板">
|
||||
<a-select
|
||||
v-model:value="form.templateId"
|
||||
:options="tplSelectOptions"
|
||||
placeholder="选择模板"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
@change="onTemplateChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="备注">
|
||||
<a-input v-model:value="form.remark" placeholder="可选" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<a-space wrap>
|
||||
<a-button type="primary" ghost @click="reloadTemplateFields" :loading="parseLoading">
|
||||
解析模板占位字段
|
||||
</a-button>
|
||||
<a-button @click="autoMatchFields" :disabled="!bizFields.length || !tplFields.length">
|
||||
同名自动匹配
|
||||
</a-button>
|
||||
</a-space>
|
||||
<a-card size="small" :bordered="true" class="bind-card">
|
||||
<template #title>
|
||||
<span class="bind-card-head">明细数据来源</span>
|
||||
<span class="bind-card-head-extra">(可选)</span>
|
||||
</template>
|
||||
<template #extra>
|
||||
<span class="bind-card-sub">有明细/表格占位时需配置</span>
|
||||
</template>
|
||||
<p class="bind-card-desc">
|
||||
选择主实体类上的明细集合属性(如 List<明细实体>)或嵌套对象;系统将明细类字段并入下方「明细与表格」中的业务字段下拉。
|
||||
</p>
|
||||
<a-select
|
||||
v-model:value="selectedDetailProperty"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
placeholder="无需明细请留空"
|
||||
:options="detailSlotSelectOptions"
|
||||
:loading="detailFieldsLoading"
|
||||
style="width: 100%"
|
||||
@change="onDetailSlotChange"
|
||||
/>
|
||||
</a-card>
|
||||
|
||||
<div v-if="tplFields.length">
|
||||
<div style="margin-bottom: 8px; font-weight: 500">字段映射</div>
|
||||
<a-table
|
||||
size="small"
|
||||
row-key="templateField"
|
||||
:pagination="false"
|
||||
:columns="mapTableColumns"
|
||||
:data-source="mappingRows"
|
||||
bordered
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'bizField'">
|
||||
<a-select
|
||||
v-model:value="record.bizField"
|
||||
:options="bizFieldOptions"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
style="width: 100%"
|
||||
placeholder="选择业务字段"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
<a-empty v-else-if="form.templateId && !parseLoading" description="请点击「解析模板占位字段」或切换模板" />
|
||||
<a-card size="small" :bordered="true" class="bind-card bind-card--mapping">
|
||||
<template #title>
|
||||
<span class="bind-card-head">字段映射</span>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-space wrap>
|
||||
<a-button type="primary" ghost size="small" @click="reloadTemplateFields" :loading="parseLoading">
|
||||
解析模板占位字段
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
@click="autoMatchFields"
|
||||
:disabled="(!bizFields.length && !detailBizFields.length) || !tplFields.length"
|
||||
>
|
||||
同名自动匹配
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-divider />
|
||||
<div style="font-weight: 500">映射预览(可选)</div>
|
||||
<a-textarea
|
||||
v-model:value="previewBizJson"
|
||||
placeholder='粘贴业务 JSON,例如:{"barcode":"TEST001","materialName":"胶料A"}'
|
||||
:rows="4"
|
||||
/>
|
||||
<a-button type="dashed" @click="runPreview" :loading="previewLoading">生成打印数据预览</a-button>
|
||||
<pre v-if="previewResult" class="preview-pre">{{ previewResult }}</pre>
|
||||
<div v-if="!form.templateId" class="bind-placeholder">请先在上方的「基础信息」中选择打印模板</div>
|
||||
|
||||
<template v-else-if="tplFields.length">
|
||||
<div class="bind-map-section">
|
||||
<div class="bind-section-bar">
|
||||
<span class="bind-section-title">① 主表参数</span>
|
||||
<span class="bind-section-hint">对应模板 dataBinding.params / 画布参数;请选择主实体 JSON 字段</span>
|
||||
</div>
|
||||
<a-table
|
||||
v-if="mappingRowsParam.length"
|
||||
size="small"
|
||||
row-key="templateField"
|
||||
:pagination="false"
|
||||
:columns="mapTableColumnsParam"
|
||||
:data-source="mappingRowsParam"
|
||||
bordered
|
||||
class="bind-map-table"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'bizField'">
|
||||
<a-select
|
||||
v-model:value="record.bizField"
|
||||
:options="bizFieldOptionsMain"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
style="width: 100%"
|
||||
placeholder="选择主表业务字段"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-empty v-else class="bind-empty" description="本模板未解析到「参数」类占位" />
|
||||
</div>
|
||||
|
||||
<div class="bind-map-section bind-map-section--detail">
|
||||
<div class="bind-section-bar">
|
||||
<span class="bind-section-title">② 明细与表格列</span>
|
||||
<span class="bind-section-hint">对应明细字段、表格列等;可选主表字段或上方明细来源生成的前缀字段</span>
|
||||
</div>
|
||||
<a-table
|
||||
v-if="mappingRowsDetail.length"
|
||||
size="small"
|
||||
row-key="templateField"
|
||||
:pagination="false"
|
||||
:columns="mapTableColumnsDetail"
|
||||
:data-source="mappingRowsDetail"
|
||||
bordered
|
||||
class="bind-map-table"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'tplKind'">
|
||||
{{ templateFieldKindLabel(record.elementType) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'bizField'">
|
||||
<a-select
|
||||
v-model:value="record.bizField"
|
||||
:options="bizFieldOptions"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
style="width: 100%"
|
||||
placeholder="选择业务字段"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-empty v-else class="bind-empty" description="本模板未解析到明细/表格列占位" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a-empty
|
||||
v-else-if="form.templateId && !parseLoading"
|
||||
class="bind-empty"
|
||||
description="请点击右上角「解析模板占位字段」或切换模板"
|
||||
/>
|
||||
</a-card>
|
||||
|
||||
<a-card title="映射预览(可选)" size="small" :bordered="true" class="bind-card">
|
||||
<a-textarea
|
||||
v-model:value="previewBizJson"
|
||||
placeholder='粘贴业务 JSON,例如:{"barcode":"TEST001","materialName":"胶料A"}'
|
||||
:rows="4"
|
||||
/>
|
||||
<div style="margin-top: 10px">
|
||||
<a-button type="dashed" @click="runPreview" :loading="previewLoading">生成打印数据预览</a-button>
|
||||
</div>
|
||||
<pre v-if="previewResult" class="preview-pre">{{ previewResult }}</pre>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-spin>
|
||||
</BasicModal>
|
||||
|
||||
<BasicModal
|
||||
@register="registerWhitelistModal"
|
||||
title="打印业务白名单"
|
||||
width="760px"
|
||||
@ok="submitWhitelistModal"
|
||||
:confirm-loading="whitelistSubmitLoading"
|
||||
destroy-on-close
|
||||
>
|
||||
<a-spin :spinning="whitelistLoading">
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
style="margin-bottom: 12px"
|
||||
message="说明"
|
||||
description="勾选允许的菜单 id,保存时会写入 print_biz_perm_entity(能推断出实体类的菜单)。打开弹窗已优化为不再加载全量业务目录。白名单为空时「新增绑定」下拉仅展示表里已有映射;白名单非空时展示勾选中且能解析的菜单。"
|
||||
/>
|
||||
<a-space style="margin-bottom: 8px">
|
||||
<a-button size="small" @click="expandWhitelistTree(true)">展开全部</a-button>
|
||||
<a-button size="small" @click="expandWhitelistTree(false)">折叠全部</a-button>
|
||||
<a-button size="small" @click="checkedKeysWhitelist = []">清空勾选</a-button>
|
||||
</a-space>
|
||||
<BasicTree
|
||||
ref="whitelistTreeRef"
|
||||
checkable
|
||||
:treeData="whitelistTreeData"
|
||||
:checkedKeys="checkedKeysWhitelist"
|
||||
:expandedKeys="expandedKeysWhitelist"
|
||||
:selectedKeys="selectedKeysWhitelist"
|
||||
:clickRowToExpand="false"
|
||||
:checkStrictly="true"
|
||||
title="系统菜单(权限树)"
|
||||
@check="onWhitelistCheck"
|
||||
>
|
||||
<template #title="{ slotTitle, ruleFlag }">
|
||||
{{ slotTitle }}
|
||||
<Icon v-if="ruleFlag" icon="ant-design:align-left-outlined" style="margin-left: 5px; color: red" />
|
||||
</template>
|
||||
</BasicTree>
|
||||
</a-spin>
|
||||
</BasicModal>
|
||||
</div>
|
||||
@@ -131,12 +272,17 @@
|
||||
import { computed, ref, unref } from 'vue';
|
||||
import { BasicTable, TableAction, useTable } from '/@/components/Table';
|
||||
import { BasicModal, useModal } from '/@/components/Modal';
|
||||
import { BasicTree, TreeItem } from '/@/components/Tree';
|
||||
import { Icon } from '/@/components/Icon';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { columns } from './bizTemplateBind.data';
|
||||
import * as Api from './bizTemplateBind.api';
|
||||
import { list as tplList } from '../template/printTemplate.api';
|
||||
import { queryTreeListForRole } from '/@/views/system/role/role.api';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const { t } = useI18n();
|
||||
|
||||
interface BizTypeItem {
|
||||
bizCode: string;
|
||||
@@ -157,9 +303,17 @@
|
||||
titleHint?: string;
|
||||
}
|
||||
|
||||
interface DetailSlotItem {
|
||||
propertyName: string;
|
||||
itemEntityClassFqn: string;
|
||||
slotKind: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const bizTypesRef = ref<BizTypeItem[]>([]);
|
||||
const tplListRef = ref<{ id: string; templateCode: string; templateName: string }[]>([]);
|
||||
const tplLoading = ref(false);
|
||||
/** 弹窗内:业务列表 + 模板下拉并行加载中(不再阻塞 openModal,避免点按钮好几秒才出框) */
|
||||
const modalDataLoading = ref(false);
|
||||
const parseLoading = ref(false);
|
||||
const modalSubmitLoading = ref(false);
|
||||
const previewLoading = ref(false);
|
||||
@@ -177,6 +331,10 @@
|
||||
const tplFields = ref<TplFieldItem[]>([]);
|
||||
const bizFields = ref<BizTypeItem['fields']>([]);
|
||||
const mappingRows = ref<MappingRow[]>([]);
|
||||
const detailSlots = ref<DetailSlotItem[]>([]);
|
||||
const selectedDetailProperty = ref<string | undefined>(undefined);
|
||||
const detailBizFields = ref<BizTypeItem['fields']>([]);
|
||||
const detailFieldsLoading = ref(false);
|
||||
|
||||
const isEditMode = ref(false);
|
||||
const modalTitle = computed(() => (unref(isEditMode) ? '编辑业务打印绑定' : '新增业务打印绑定'));
|
||||
@@ -195,16 +353,61 @@
|
||||
})),
|
||||
);
|
||||
|
||||
const bizFieldOptions = computed(() =>
|
||||
const detailSlotSelectOptions = computed(() =>
|
||||
unref(detailSlots).map((s) => ({
|
||||
label: `${s.label}(${s.propertyName} · ${s.slotKind})`,
|
||||
value: s.propertyName,
|
||||
})),
|
||||
);
|
||||
|
||||
const bizFieldOptionsMain = computed(() =>
|
||||
unref(bizFields).map((f) => ({
|
||||
label: f.label ? `${f.label}(${f.fieldKey})` : f.fieldKey,
|
||||
value: f.fieldKey,
|
||||
})),
|
||||
);
|
||||
|
||||
const mapTableColumns = [
|
||||
{ title: '模板占位(bindField)', dataIndex: 'templateField', width: 220 },
|
||||
{ title: '类型', dataIndex: 'elementType', width: 100 },
|
||||
/** 主表 + 明细前缀字段(用于明细/表格占位) */
|
||||
const bizFieldOptions = computed(() => {
|
||||
const main = unref(bizFields).map((f) => ({
|
||||
label: f.label ? `${f.label}(${f.fieldKey})` : f.fieldKey,
|
||||
value: f.fieldKey,
|
||||
}));
|
||||
const detail = unref(detailBizFields).map((f) => ({
|
||||
label: f.label ? `${f.label}(${f.fieldKey})` : f.fieldKey,
|
||||
value: f.fieldKey,
|
||||
}));
|
||||
return [...main, ...detail];
|
||||
});
|
||||
|
||||
/** 主表参数行:模板 elementType 为 param */
|
||||
const mappingRowsParam = computed(() =>
|
||||
unref(mappingRows).filter((r) => (r.elementType || '') === 'param'),
|
||||
);
|
||||
|
||||
/** 非参数占位(明细字段、表格列、其它画布元素) */
|
||||
const mappingRowsDetail = computed(() =>
|
||||
unref(mappingRows).filter((r) => (r.elementType || '') !== 'param'),
|
||||
);
|
||||
|
||||
function templateFieldKindLabel(t?: string) {
|
||||
const m: Record<string, string> = {
|
||||
param: '主表·参数',
|
||||
detailField: '模板·明细',
|
||||
column: '表格列',
|
||||
};
|
||||
return m[t || ''] || (t || '—');
|
||||
}
|
||||
|
||||
const mapTableColumnsParam = [
|
||||
{ title: '模板参数占位(bindField)', dataIndex: 'templateField', width: 220 },
|
||||
{ title: '标题/提示', dataIndex: 'titleHint', ellipsis: true },
|
||||
{ title: '业务字段(主表)', key: 'bizField', width: 280 },
|
||||
];
|
||||
|
||||
const mapTableColumnsDetail = [
|
||||
{ title: '模板类型', key: 'tplKind', width: 104, ellipsis: true },
|
||||
{ title: '模板占位(bindField)', dataIndex: 'templateField', width: 188 },
|
||||
{ title: '标题/提示', dataIndex: 'titleHint', ellipsis: true },
|
||||
{ title: '业务字段', key: 'bizField', width: 260 },
|
||||
];
|
||||
@@ -228,26 +431,138 @@
|
||||
});
|
||||
|
||||
const [registerModal, { openModal, closeModal }] = useModal();
|
||||
const [registerWhitelistModal, { openModal: openWhitelistDlg, closeModal: closeWhitelistDlg }] = useModal();
|
||||
|
||||
const whitelistLoading = ref(false);
|
||||
const whitelistSubmitLoading = ref(false);
|
||||
const whitelistTreeRef = ref<InstanceType<typeof BasicTree> | null>(null);
|
||||
const whitelistTreeData = ref<TreeItem[]>([]);
|
||||
const allWhitelistTreeKeys = ref<string[]>([]);
|
||||
const checkedKeysWhitelist = ref<any>([]);
|
||||
const expandedKeysWhitelist = ref<any>([]);
|
||||
const selectedKeysWhitelist = ref<any>([]);
|
||||
|
||||
/** 将菜单树标题中的 t('...') 表达式转为文案(与角色授权树一致) */
|
||||
function translateWhitelistTitle(data: TreeItem[] | undefined): TreeItem[] {
|
||||
if (data?.length) {
|
||||
data.forEach((item) => {
|
||||
if (item.slotTitle && typeof item.slotTitle === 'string' && item.slotTitle.includes("t('")) {
|
||||
try {
|
||||
item.slotTitle = new Function('t', `return ${item.slotTitle}`)(t) as string;
|
||||
} catch {
|
||||
/* 忽略解析失败 */
|
||||
}
|
||||
}
|
||||
if (item.children?.length) {
|
||||
translateWhitelistTitle(item.children as TreeItem[]);
|
||||
}
|
||||
});
|
||||
}
|
||||
return data ?? [];
|
||||
}
|
||||
|
||||
function expandWhitelistTree(expand: boolean) {
|
||||
expandedKeysWhitelist.value = expand ? [...unref(allWhitelistTreeKeys)] : [];
|
||||
}
|
||||
|
||||
function onWhitelistCheck(o: { checked?: any } | any) {
|
||||
checkedKeysWhitelist.value = o?.checked !== undefined ? o.checked : o;
|
||||
}
|
||||
|
||||
/** 树勾选键转为字符串数组(兼容 checkStrictly) */
|
||||
function normalizeCheckedKeys(keys: unknown): string[] {
|
||||
if (Array.isArray(keys)) {
|
||||
return keys.map((k) => String(k));
|
||||
}
|
||||
if (keys && typeof keys === 'object' && 'checked' in (keys as object)) {
|
||||
const c = (keys as { checked?: unknown }).checked;
|
||||
if (Array.isArray(c)) {
|
||||
return c.map((k) => String(k));
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async function openPrintBizWhitelist() {
|
||||
whitelistLoading.value = true;
|
||||
try {
|
||||
const [treeResult, wl] = await Promise.all([queryTreeListForRole(), Api.getPermWhitelist()]);
|
||||
whitelistTreeData.value = translateWhitelistTitle(treeResult.treeList);
|
||||
allWhitelistTreeKeys.value = treeResult.ids || [];
|
||||
expandedKeysWhitelist.value = treeResult.ids || [];
|
||||
checkedKeysWhitelist.value = wl?.permIds?.length ? [...wl.permIds] : [];
|
||||
openWhitelistDlg(true);
|
||||
} finally {
|
||||
whitelistLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitWhitelistModal() {
|
||||
whitelistSubmitLoading.value = true;
|
||||
try {
|
||||
const tree = unref(whitelistTreeRef) as any;
|
||||
let raw = tree?.getCheckedKeys?.() ?? unref(checkedKeysWhitelist);
|
||||
const permIds = normalizeCheckedKeys(raw);
|
||||
await Api.savePermWhitelist({ permIds });
|
||||
createMessage.success('保存成功');
|
||||
closeWhitelistDlg();
|
||||
} finally {
|
||||
whitelistSubmitLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBizTypes() {
|
||||
const res = await Api.bizTypes();
|
||||
const res = await Api.bizTypesForBinding();
|
||||
bizTypesRef.value = res || [];
|
||||
}
|
||||
|
||||
async function loadAllTemplates() {
|
||||
tplLoading.value = true;
|
||||
const res = await tplList({ pageNo: 1, pageSize: 500 });
|
||||
tplListRef.value = res?.records ?? [];
|
||||
}
|
||||
|
||||
async function refreshDetailSlots(code: string | undefined) {
|
||||
detailSlots.value = [];
|
||||
selectedDetailProperty.value = undefined;
|
||||
detailBizFields.value = [];
|
||||
if (!code) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await tplList({ pageNo: 1, pageSize: 500 });
|
||||
tplListRef.value = res?.records ?? [];
|
||||
} finally {
|
||||
tplLoading.value = false;
|
||||
detailSlots.value = (await Api.detailSlots(code)) || [];
|
||||
} catch {
|
||||
detailSlots.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function onBizCodeChange(code: string) {
|
||||
async function onDetailSlotChange(propertyName: string | undefined) {
|
||||
selectedDetailProperty.value = propertyName;
|
||||
if (!propertyName || !form.value.bizCode) {
|
||||
detailBizFields.value = [];
|
||||
return;
|
||||
}
|
||||
const slot = unref(detailSlots).find((s) => s.propertyName === propertyName);
|
||||
const kind = slot?.slotKind || 'LIST';
|
||||
detailFieldsLoading.value = true;
|
||||
try {
|
||||
const list = await Api.bizFieldsForDetailSlot({
|
||||
bizCode: form.value.bizCode,
|
||||
detailProperty: propertyName,
|
||||
slotKind: kind,
|
||||
});
|
||||
detailBizFields.value = (list || []) as BizTypeItem['fields'];
|
||||
} catch {
|
||||
detailBizFields.value = [];
|
||||
} finally {
|
||||
detailFieldsLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onBizCodeChange(code: string) {
|
||||
const hit = unref(bizTypesRef).find((b) => b.bizCode === code);
|
||||
bizFields.value = hit?.fields ?? [];
|
||||
form.value.bizName = hit?.bizName;
|
||||
await refreshDetailSlots(code);
|
||||
}
|
||||
|
||||
async function onTemplateChange() {
|
||||
@@ -290,7 +605,8 @@
|
||||
const savedMappingRef = ref<{ templateField: string; bizField?: string }[]>([]);
|
||||
|
||||
function autoMatchFields() {
|
||||
const set = new Map(unref(bizFields).map((f) => [f.fieldKey, f.fieldKey]));
|
||||
const merged = [...unref(bizFields), ...unref(detailBizFields)];
|
||||
const set = new Map(merged.map((f) => [f.fieldKey, f.fieldKey]));
|
||||
for (const row of unref(mappingRows)) {
|
||||
if (set.has(row.templateField)) {
|
||||
row.bizField = row.templateField;
|
||||
@@ -313,17 +629,27 @@
|
||||
tplFields.value = [];
|
||||
bizFields.value = [];
|
||||
mappingRows.value = [];
|
||||
detailSlots.value = [];
|
||||
selectedDetailProperty.value = undefined;
|
||||
detailBizFields.value = [];
|
||||
previewBizJson.value = '';
|
||||
previewResult.value = '';
|
||||
await loadBizTypes();
|
||||
await loadAllTemplates();
|
||||
openModal(true);
|
||||
modalDataLoading.value = true;
|
||||
try {
|
||||
await Promise.all([loadBizTypes(), loadAllTemplates()]);
|
||||
} finally {
|
||||
modalDataLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function openEdit(record: Recordable) {
|
||||
isEditMode.value = true;
|
||||
await loadBizTypes();
|
||||
await loadAllTemplates();
|
||||
try {
|
||||
savedMappingRef.value = JSON.parse(record.fieldMappingJson || '[]');
|
||||
} catch {
|
||||
savedMappingRef.value = [];
|
||||
}
|
||||
form.value = {
|
||||
id: record.id,
|
||||
bizCode: record.bizCode,
|
||||
@@ -331,16 +657,23 @@
|
||||
templateId: record.templateId,
|
||||
remark: record.remark,
|
||||
};
|
||||
onBizCodeChange(record.bizCode);
|
||||
try {
|
||||
savedMappingRef.value = JSON.parse(record.fieldMappingJson || '[]');
|
||||
} catch {
|
||||
savedMappingRef.value = [];
|
||||
}
|
||||
previewBizJson.value = '';
|
||||
previewResult.value = '';
|
||||
tplFields.value = [];
|
||||
mappingRows.value = [];
|
||||
bizFields.value = [];
|
||||
detailSlots.value = [];
|
||||
selectedDetailProperty.value = undefined;
|
||||
detailBizFields.value = [];
|
||||
openModal(true);
|
||||
await reloadTemplateFields();
|
||||
modalDataLoading.value = true;
|
||||
try {
|
||||
await Promise.all([loadBizTypes(), loadAllTemplates()]);
|
||||
await onBizCodeChange(record.bizCode as string);
|
||||
await reloadTemplateFields();
|
||||
} finally {
|
||||
modalDataLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitModal() {
|
||||
@@ -403,6 +736,104 @@
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.biz-bind-form {
|
||||
max-height: calc(100vh - 220px);
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.bind-alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.bind-card {
|
||||
margin-bottom: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.bind-card :deep(.ant-card-head) {
|
||||
min-height: 42px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.bind-card :deep(.ant-card-body) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.bind-card-form :deep(.ant-form-item) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.bind-card-head {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.bind-card-head-extra {
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.bind-card-sub {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.bind-card-desc {
|
||||
margin: 0 0 10px;
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.bind-placeholder {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.bind-map-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.bind-map-section--detail {
|
||||
margin-bottom: 0;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed #f0f0f0;
|
||||
}
|
||||
|
||||
.bind-section-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 8px 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.bind-section-title {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.bind-section-hint {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.bind-map-table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.bind-empty {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.preview-pre {
|
||||
background: #f5f5f5;
|
||||
padding: 12px;
|
||||
@@ -410,5 +841,6 @@
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
font-size: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,6 +12,9 @@ enum Api {
|
||||
batchStockIn = '/xslmes/mesXslRawMaterialEntry/batchStockIn',
|
||||
importExcel = '/xslmes/mesXslRawMaterialEntry/importExcel',
|
||||
exportXls = '/xslmes/mesXslRawMaterialEntry/exportXls',
|
||||
queryPrinters = '/xslmes/mesXslRawMaterialEntry/queryPrinters',
|
||||
prepareNativePrint = '/xslmes/mesXslRawMaterialEntry/prepareNativePrint',
|
||||
printPdf = '/xslmes/mesXslRawMaterialEntry/printPdf',
|
||||
}
|
||||
|
||||
export const getExportUrl = Api.exportXls;
|
||||
@@ -47,3 +50,15 @@ export const saveOrUpdate = (params, isUpdate) => {
|
||||
let url = isUpdate ? Api.edit : Api.save;
|
||||
return defHttp.post({ url: url, params });
|
||||
};
|
||||
|
||||
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 });
|
||||
|
||||
@@ -6,6 +6,51 @@
|
||||
<a-button type="primary" v-auth="'xslmes:mes_xsl_raw_material_entry:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
|
||||
<j-upload-button type="primary" v-auth="'xslmes:mes_xsl_raw_material_entry:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
|
||||
<a-button type="primary" v-auth="'xslmes:mes_xsl_raw_material_entry:stockIn'" preIcon="ant-design:check-circle-outlined" @click="handleStockIn"> 结存入库</a-button>
|
||||
<a-checkbox v-model:checked="printDotEnabled" style="margin-left: 8px" @change="onPrintDotEnabledChange">
|
||||
PrintDot 桥接
|
||||
</a-checkbox>
|
||||
<a-input
|
||||
v-model:value="printDotWsUrl"
|
||||
style="width: 220px; margin-left: 8px"
|
||||
placeholder="ws://127.0.0.1:1122/ws"
|
||||
@blur="persistPrintDotConfig"
|
||||
/>
|
||||
<a-input-password
|
||||
v-model:value="printDotKey"
|
||||
style="width: 130px; margin-left: 8px"
|
||||
placeholder="密钥(可选)"
|
||||
autocomplete="new-password"
|
||||
@blur="persistPrintDotConfig"
|
||||
/>
|
||||
<a-button @click="downloadPrintPlugin">下载打印插件</a-button>
|
||||
<a-select
|
||||
v-model:value="selectedPrinterName"
|
||||
:options="printerOptions"
|
||||
style="width: 220px; margin-left: 8px"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
:placeholder="printerSelectPlaceholder"
|
||||
/>
|
||||
<a-button @click="() => refreshPrinterOptions(true)">刷新打印机</a-button>
|
||||
<a-input
|
||||
v-model:value="manualPrinterName"
|
||||
style="width: 150px; margin-left: 8px"
|
||||
placeholder="手动输入打印机名"
|
||||
@press-enter="addManualPrinter"
|
||||
/>
|
||||
<a-button @click="addManualPrinter">添加</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
v-auth="'xslmes:mes_xsl_raw_material_entry:edit'"
|
||||
:loading="printLoading"
|
||||
:disabled="selectedRowKeys.length === 0 || !printDotEnabled"
|
||||
@click="handlePrintSelected"
|
||||
>
|
||||
<Icon icon="ant-design:printer-outlined" />
|
||||
打印选中
|
||||
</a-button>
|
||||
<a-dropdown v-if="selectedRowKeys.length > 0">
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
@@ -26,24 +71,77 @@
|
||||
</template>
|
||||
</BasicTable>
|
||||
<MesXslRawMaterialEntryModal @register="registerModal" @success="handleSuccess" />
|
||||
<RawMaterialEntryPrintPreviewModal
|
||||
v-model:open="printPreviewOpen"
|
||||
:entry-id="printPreviewEntryId"
|
||||
:barcode="printPreviewBarcode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="xslmes-mesXslRawMaterialEntry" setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { BasicTable, useTable, TableAction } from '/@/components/Table';
|
||||
import { onMounted, ref, reactive, watch } from 'vue';
|
||||
import { Icon } from '/@/components/Icon';
|
||||
import { BasicTable, TableAction } from '/@/components/Table';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
import MesXslRawMaterialEntryModal from './components/MesXslRawMaterialEntryModal.vue';
|
||||
import RawMaterialEntryPrintPreviewModal from './components/RawMaterialEntryPrintPreviewModal.vue';
|
||||
import { columns, searchFormSchema, superQuerySchema } from './MesXslRawMaterialEntry.data';
|
||||
import { list, deleteOne, batchDelete, batchStockIn, getImportUrl, getExportUrl } from './MesXslRawMaterialEntry.api';
|
||||
import {
|
||||
list,
|
||||
deleteOne,
|
||||
batchDelete,
|
||||
batchStockIn,
|
||||
getImportUrl,
|
||||
getExportUrl,
|
||||
prepareNativePrint,
|
||||
} from './MesXslRawMaterialEntry.api';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import {
|
||||
PRINT_TEMPLATE_SELECTED_PRINTER_KEY,
|
||||
printNativeSchemaViaPrintDot,
|
||||
} from '/@/views/print/template/utils/printNativeViaPrintDot';
|
||||
import { normalizeImportedNativeSchema } from '/@/views/print/template/native/core/nativeSchemaNormalize';
|
||||
import {
|
||||
fetchPrintDotPrinters,
|
||||
getPrintDotBridgeConfig,
|
||||
setPrintDotBridgeConfig,
|
||||
} from '/@/views/print/template/utils/printDotBridge';
|
||||
|
||||
const { createConfirm, createMessage } = useMessage();
|
||||
const LS_PRINT_DOT_ENABLED = 'qhmes_print_dot_enabled';
|
||||
const printDotEnabled = ref(localStorage.getItem(LS_PRINT_DOT_ENABLED) !== '0');
|
||||
const printDotCfg = getPrintDotBridgeConfig();
|
||||
const printDotWsUrl = ref(printDotCfg.wsUrl);
|
||||
const printDotKey = ref(printDotCfg.key);
|
||||
|
||||
function persistPrintDotConfig() {
|
||||
setPrintDotBridgeConfig(printDotWsUrl.value, printDotKey.value);
|
||||
void refreshPrinterOptions(false);
|
||||
}
|
||||
|
||||
function onPrintDotEnabledChange() {
|
||||
localStorage.setItem(LS_PRINT_DOT_ENABLED, printDotEnabled.value ? '1' : '0');
|
||||
}
|
||||
|
||||
function downloadPrintPlugin() {
|
||||
const base = import.meta.env.BASE_URL || '/';
|
||||
const normalizedBase = base.endsWith('/') ? base : `${base}/`;
|
||||
const url = `${normalizedBase}print-plugin/XSL-PrintDot.exe`;
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', 'XSL-PrintDot.exe');
|
||||
link.rel = 'noopener';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
const queryParam = reactive<any>({});
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
|
||||
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
|
||||
const { tableContext, onExportXls, onImportXls } = useListPage({
|
||||
tableProps: {
|
||||
title: '原料入场记录',
|
||||
api: list,
|
||||
@@ -58,8 +156,11 @@
|
||||
],
|
||||
},
|
||||
actionColumn: {
|
||||
width: 180,
|
||||
width: 320,
|
||||
fixed: 'right',
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
slots: { customRender: 'action' },
|
||||
},
|
||||
beforeFetch: (params) => {
|
||||
return Object.assign(params, queryParam);
|
||||
@@ -76,9 +177,201 @@
|
||||
},
|
||||
});
|
||||
|
||||
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
|
||||
const [registerTable, { reload }, { rowSelection, selectedRowKeys, selectedRows }] = tableContext;
|
||||
const superQueryConfig = reactive(superQuerySchema);
|
||||
|
||||
/** 打印预览弹窗 */
|
||||
const printPreviewOpen = ref(false);
|
||||
const printPreviewEntryId = ref<string | null>(null);
|
||||
const printPreviewBarcode = ref<string | undefined>(undefined);
|
||||
|
||||
function handlePrintPreview(record: Recordable) {
|
||||
printPreviewEntryId.value = record.id as string;
|
||||
printPreviewBarcode.value = record.barcode as string | undefined;
|
||||
printPreviewOpen.value = true;
|
||||
}
|
||||
|
||||
const printerOptions = ref<Array<{ label: string; value: string }>>([]);
|
||||
const selectedPrinterName = ref<string>('__system_default__');
|
||||
const manualPrinterName = ref('');
|
||||
const printLoading = ref(false);
|
||||
const printerSelectPlaceholder = '选择打印机(PrintDot 桥接)';
|
||||
|
||||
watch(selectedPrinterName, (v) => {
|
||||
if (v) {
|
||||
localStorage.setItem(PRINT_TEMPLATE_SELECTED_PRINTER_KEY, v);
|
||||
}
|
||||
});
|
||||
|
||||
async function refreshPrinterOptions(showMessage = true) {
|
||||
const optionMap = new Map<string, { label: string; value: string }>();
|
||||
optionMap.set('__system_default__', { label: '系统默认打印机', value: '__system_default__' });
|
||||
try {
|
||||
const dotList = await fetchPrintDotPrinters();
|
||||
dotList.forEach((p) => {
|
||||
const name = String(p.name || '').trim();
|
||||
if (!name) return;
|
||||
const defMark = p.isDefault ? '(默认)' : '';
|
||||
optionMap.set(name, { label: `${name}${defMark}`, value: name });
|
||||
});
|
||||
if (selectedPrinterName.value && !optionMap.has(selectedPrinterName.value)) {
|
||||
optionMap.set(selectedPrinterName.value, {
|
||||
label: `${selectedPrinterName.value}(手动)`,
|
||||
value: selectedPrinterName.value,
|
||||
});
|
||||
}
|
||||
printerOptions.value = Array.from(optionMap.values());
|
||||
if (showMessage) {
|
||||
if (dotList.length) {
|
||||
createMessage.success(`已从 PrintDot 桥接识别 ${dotList.length} 台打印机`);
|
||||
} else {
|
||||
createMessage.warning('PrintDot 已连接但未返回打印机列表');
|
||||
}
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (selectedPrinterName.value && !optionMap.has(selectedPrinterName.value)) {
|
||||
optionMap.set(selectedPrinterName.value, {
|
||||
label: `${selectedPrinterName.value}(手动)`,
|
||||
value: selectedPrinterName.value,
|
||||
});
|
||||
}
|
||||
printerOptions.value = Array.from(optionMap.values());
|
||||
if (showMessage) {
|
||||
createMessage.warning(`PrintDot:${e instanceof Error ? e.message : String(e)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addManualPrinter() {
|
||||
const name = String(manualPrinterName.value || '').trim();
|
||||
if (!name) return;
|
||||
const exists = printerOptions.value.some((item) => item.value === name);
|
||||
if (!exists) {
|
||||
printerOptions.value = [...printerOptions.value, { label: `${name}(手动)`, value: name }];
|
||||
}
|
||||
selectedPrinterName.value = name;
|
||||
manualPrinterName.value = '';
|
||||
createMessage.success('已添加打印机');
|
||||
}
|
||||
|
||||
async function executePrint(record: Recordable, options?: { silentSuccess?: boolean }) {
|
||||
try {
|
||||
const prep = (await prepareNativePrint(record.id as string)) as Record<string, unknown>;
|
||||
const templateJsonRaw = prep.templateJson as string;
|
||||
const printData = prep.printData as Record<string, unknown>;
|
||||
const paperWidthMm = Number((prep as any).paperWidthMm ?? 0);
|
||||
const paperHeightMm = Number((prep as any).paperHeightMm ?? 0);
|
||||
const paperOrientation = String((prep as any).paperOrientation || '').toLowerCase();
|
||||
if (!templateJsonRaw) {
|
||||
throw new Error('模板 JSON 为空');
|
||||
}
|
||||
let raw: unknown;
|
||||
try {
|
||||
raw = typeof templateJsonRaw === 'string' ? JSON.parse(templateJsonRaw) : templateJsonRaw;
|
||||
} catch {
|
||||
throw new Error('模板 JSON 格式错误');
|
||||
}
|
||||
const schema = normalizeImportedNativeSchema(raw);
|
||||
if (paperWidthMm > 0 && paperHeightMm > 0) {
|
||||
const orient = paperOrientation === 'landscape' ? 'landscape' : paperOrientation === 'portrait' ? 'portrait' : '';
|
||||
const normalized =
|
||||
orient === 'landscape'
|
||||
? {
|
||||
width: Math.max(paperWidthMm, paperHeightMm),
|
||||
height: Math.min(paperWidthMm, paperHeightMm),
|
||||
}
|
||||
: orient === 'portrait'
|
||||
? {
|
||||
width: Math.min(paperWidthMm, paperHeightMm),
|
||||
height: Math.max(paperWidthMm, paperHeightMm),
|
||||
}
|
||||
: {
|
||||
width: paperWidthMm,
|
||||
height: paperHeightMm,
|
||||
};
|
||||
schema.page.width = normalized.width;
|
||||
schema.page.height = normalized.height;
|
||||
}
|
||||
await printNativeSchemaViaPrintDot({
|
||||
schema,
|
||||
data: printData as Record<string, unknown>,
|
||||
jobName: `原料入场记录-${(record.barcode as string) || record.id}.pdf`,
|
||||
printerSelection:
|
||||
selectedPrinterName.value ||
|
||||
localStorage.getItem(PRINT_TEMPLATE_SELECTED_PRINTER_KEY) ||
|
||||
'__system_default__',
|
||||
});
|
||||
if (!options?.silentSuccess) {
|
||||
createMessage.success('已通过 PrintDot 提交打印');
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
throw new Error(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
}
|
||||
|
||||
function handlePrintSelected() {
|
||||
if (!printDotEnabled.value) {
|
||||
createMessage.warning('请先开启 PrintDot 桥接');
|
||||
return;
|
||||
}
|
||||
const rows = selectedRows.value || [];
|
||||
if (!rows.length) {
|
||||
createMessage.warning('请至少勾选一条记录后再点击「打印选中」');
|
||||
return;
|
||||
}
|
||||
printLoading.value = true;
|
||||
const hideLoadingMsg = createMessage.loading(`正在打印 ${rows.length} 条记录,请稍候…`, 0);
|
||||
(async () => {
|
||||
let ok = 0;
|
||||
let firstError = '';
|
||||
for (const row of rows) {
|
||||
try {
|
||||
await executePrint(row, { silentSuccess: true });
|
||||
ok += 1;
|
||||
} catch (e: unknown) {
|
||||
if (!firstError) {
|
||||
firstError = e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ok === rows.length) {
|
||||
createMessage.success(`已通过 PrintDot 提交 ${ok} 条打印任务`);
|
||||
} else {
|
||||
createMessage.warning(`打印完成:成功 ${ok},失败 ${rows.length - ok}${firstError ? `。首条错误:${firstError}` : ''}`);
|
||||
}
|
||||
hideLoadingMsg();
|
||||
printLoading.value = false;
|
||||
})();
|
||||
}
|
||||
|
||||
function handlePrintRow(record: Recordable) {
|
||||
if (!printDotEnabled.value) {
|
||||
createMessage.warning('请先开启 PrintDot 桥接');
|
||||
return;
|
||||
}
|
||||
printLoading.value = true;
|
||||
const hideLoadingMsg = createMessage.loading('正在生成 PDF 并提交打印,版面复杂时可能需数十秒,请稍候…', 0);
|
||||
executePrint(record)
|
||||
.then(() => {
|
||||
createMessage.success('已通过 PrintDot 提交打印');
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
createMessage.error(e instanceof Error ? e.message : String(e));
|
||||
})
|
||||
.finally(() => {
|
||||
hideLoadingMsg();
|
||||
printLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const saved = localStorage.getItem(PRINT_TEMPLATE_SELECTED_PRINTER_KEY);
|
||||
if (saved) {
|
||||
selectedPrinterName.value = saved;
|
||||
}
|
||||
refreshPrinterOptions(false);
|
||||
});
|
||||
|
||||
function handleSuperQuery(params) {
|
||||
Object.keys(params).map((k) => {
|
||||
queryParam[k] = params[k];
|
||||
@@ -136,6 +429,16 @@
|
||||
onClick: handleEdit.bind(null, record),
|
||||
auth: 'xslmes:mes_xsl_raw_material_entry:edit',
|
||||
},
|
||||
{
|
||||
label: '打印预览',
|
||||
onClick: handlePrintPreview.bind(null, record),
|
||||
auth: 'xslmes:mes_xsl_raw_material_entry:edit',
|
||||
},
|
||||
{
|
||||
label: '打印',
|
||||
onClick: handlePrintRow.bind(null, record),
|
||||
auth: 'xslmes:mes_xsl_raw_material_entry:edit',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="innerOpen"
|
||||
:title="modalTitle"
|
||||
width="960px"
|
||||
:footer="null"
|
||||
destroy-on-close
|
||||
wrap-class-name="raw-material-entry-print-preview-modal"
|
||||
@cancel="onClose"
|
||||
>
|
||||
<a-spin :spinning="loading">
|
||||
<div v-if="errorText" class="preview-error">{{ errorText }}</div>
|
||||
<div v-else class="preview-body">
|
||||
<iframe
|
||||
v-if="previewHtml"
|
||||
ref="previewIframeRef"
|
||||
class="preview-iframe"
|
||||
title="原料入场记录打印预览"
|
||||
:srcdoc="previewHtml"
|
||||
/>
|
||||
<a-empty v-else-if="!loading" description="暂无预览内容" />
|
||||
</div>
|
||||
</a-spin>
|
||||
<div class="preview-footer">
|
||||
<a-space>
|
||||
<a-button @click="innerOpen = false">关闭</a-button>
|
||||
<a-button type="primary" :disabled="!previewHtml || !!errorText" @click="handleBrowserPrint">
|
||||
<Icon icon="ant-design:printer-outlined" />
|
||||
浏览器打印
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Icon } from '/@/components/Icon';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { prepareNativePrint } from '../MesXslRawMaterialEntry.api';
|
||||
import { renderNativePrintHtml } from '/@/views/print/template/native/core/printRenderer';
|
||||
import { normalizeImportedNativeSchema } from '/@/views/print/template/native/core/nativeSchemaNormalize';
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
/** 入场记录主键 */
|
||||
entryId: string | null;
|
||||
/** 展示在标题上的条码/说明 */
|
||||
barcode?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:open', v: boolean): void;
|
||||
}>();
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const innerOpen = computed({
|
||||
get: () => props.open,
|
||||
set: (v: boolean) => emit('update:open', v),
|
||||
});
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
const b = String(props.barcode || '').trim();
|
||||
return b ? `原料入场记录打印预览(条码:${b})` : '原料入场记录打印预览';
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const errorText = ref('');
|
||||
const previewHtml = ref('');
|
||||
const previewIframeRef = ref<HTMLIFrameElement | null>(null);
|
||||
|
||||
async function loadPreview(id: string) {
|
||||
loading.value = true;
|
||||
errorText.value = '';
|
||||
previewHtml.value = '';
|
||||
try {
|
||||
const prep = (await prepareNativePrint(id)) as Record<string, unknown>;
|
||||
const templateJsonRaw = prep.templateJson as string;
|
||||
const printData = prep.printData as Record<string, unknown>;
|
||||
if (!templateJsonRaw) {
|
||||
throw new Error('模板 JSON 为空,请检查「业务打印绑定」是否已配置');
|
||||
}
|
||||
let raw: unknown;
|
||||
try {
|
||||
raw = typeof templateJsonRaw === 'string' ? JSON.parse(templateJsonRaw) : templateJsonRaw;
|
||||
} catch {
|
||||
throw new Error('模板 JSON 格式错误');
|
||||
}
|
||||
const schema = normalizeImportedNativeSchema(raw);
|
||||
previewHtml.value = await renderNativePrintHtml(schema, printData as Record<string, unknown>);
|
||||
} catch (e: unknown) {
|
||||
errorText.value = e instanceof Error ? e.message : String(e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 Modal 内对 iframe 直接 print() 时,打印对话框易被遮罩层级挡住或焦点异常,表现为「点了没反应」。
|
||||
* 改为在 body 下挂临时 iframe、写入同一套 HTML 再打印,与模板预览常用做法一致。
|
||||
*/
|
||||
function handleBrowserPrint() {
|
||||
const html = previewHtml.value;
|
||||
if (!html?.trim()) {
|
||||
createMessage.warning('预览未就绪,请稍后再试');
|
||||
return;
|
||||
}
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.setAttribute(
|
||||
'style',
|
||||
'position:fixed;left:0;top:0;width:0;height:0;border:0;opacity:0;pointer-events:none;',
|
||||
);
|
||||
document.body.appendChild(iframe);
|
||||
const doc = iframe.contentDocument;
|
||||
if (!doc) {
|
||||
document.body.removeChild(iframe);
|
||||
createMessage.error('无法创建打印文档');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
doc.open();
|
||||
doc.write(html);
|
||||
doc.close();
|
||||
} catch {
|
||||
document.body.removeChild(iframe);
|
||||
createMessage.error('写入打印内容失败');
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
try {
|
||||
if (iframe.parentNode) {
|
||||
document.body.removeChild(iframe);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const runPrint = () => {
|
||||
try {
|
||||
const w = iframe.contentWindow;
|
||||
if (!w) {
|
||||
createMessage.error('无法唤起打印窗口');
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
w.focus();
|
||||
w.print();
|
||||
/** 关闭打印对话框后移除临时 iframe(部分浏览器支持 afterprint) */
|
||||
w.addEventListener('afterprint', cleanup, { once: true });
|
||||
window.setTimeout(cleanup, 120000);
|
||||
} catch {
|
||||
createMessage.error('无法唤起打印,请检查浏览器弹窗/打印权限');
|
||||
cleanup();
|
||||
}
|
||||
};
|
||||
|
||||
/** 等待排版与字体后再打印,减少空白页 */
|
||||
window.setTimeout(runPrint, 100);
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
errorText.value = '';
|
||||
previewHtml.value = '';
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.open, props.entryId] as const,
|
||||
([isOpen, id]) => {
|
||||
if (isOpen && id) {
|
||||
void loadPreview(id);
|
||||
}
|
||||
if (!isOpen) {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.preview-error {
|
||||
color: #cf1322;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.preview-body {
|
||||
min-height: 420px;
|
||||
max-height: 72vh;
|
||||
overflow: auto;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 4px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.preview-iframe {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
border: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.preview-footer {
|
||||
margin-top: 16px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user