当前位置: 首页 > news >正文

为iBizPLM构建`知识库导出`插件

为iBizPLM构建知识库导出插件

概述

本文详细介绍了如何开发一个专为iBizPLM系统设计的知识库导出插件。该工具能够将iBizPLM知识库中的页面、文档等信息高效、规范地导出为多种可存档格式(如PDF、HTML、Markdown等),满足知识沉淀、离线阅读、合规审计等业务场景需求。

一、背景与需求

1.1 项目背景

iBizPLM系统作为产品生命周期管理的核心平台,积累了大量的产品知识、项目文档和技术资料。这些知识资产存储于系统的知识库模块中。在日常运营中,企业经常需要将特定的知识库内容导出为标准化格式,用于对外交付、内部培训、资料归档或灾难备份,从而确保知识的可持续性和可移植性。

1.2 核心需求

  • 多格式导出:支持将知识库页面导出为PDF、HTML、Markdown、Word等常见格式。
  • 批量操作:支持按目录、项目或标签批量选择页面进行导出。
  • 内容保真:确保导出内容与在线浏览时的一致性,包括文本、图片、表格、附件等。
  • 权限集成:导出的权限控制应与PLM系统本身的页面访问权限一致,禁止越权导出。

二、系统设计

该插件是一个具备功能增强插件,一般在相关数据的默认操作区附加操作功能

2.1 知识库一建导出

在知识库主界面提供一建导出功能。下载的ZIP文件中,同时提供了每页的独立PDF与一份带书签目录的合并PDF。
在这里插入图片描述

2.1 页面一建导出

在页面目录树提供一建导出功能。
在这里插入图片描述

三、建模开发

本次建模将涉及实体(PSDATAENTITY)、界面行为(PSDEUIACTION)、界面行为组(PSDEUAGROUP)、数据导出(PSDEDATAEXP)。点击访问建模系统,察看完整模型信息。

3.1 导入主系统实体

导入需要的主系统实体,如IDEAPAGE等,导入模型是引入主系统的能力定义,并不实际提供能力
在这里插入图片描述
设置产品管理等主系统模块功能类型为主系统代理,该模块里面的实体运行时将由主系统提供
在这里插入图片描述

3.2 建立扩展实体

建立扩展实体UX_IDEAUX_PAGE,由于主系统代理的实体仅能注册特定模型至主系统,其它模型功能无法启用,我们需要插件自身扩展实体承担功能

3.3 建立数据导出

分别为实体UX_IDEAUX_PAGE建立数据导出
在这里插入图片描述

3.3.1 自定义导出处理插件

自定义数据导出处理插件(继承自net.ibizsys.central.plugin.poi.dataentity.dataexport.POIDEDataExportRuntime),插件使用groovy语言,支持热编译及预编译两种模式,可在开发工具中进行代码调试。

在这里插入图片描述
下面仅示例IdeaDataExportRuntimeEx,完整请点击访问建模系统

package cn.ibizlab.plm.user.plugin.groovy.dataentity.dataexportimport com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder
import com.vladsch.flexmark.html.HtmlRenderer
import com.vladsch.flexmark.parser.Parser
import groovy.transform.CompileStatic
import net.ibizsys.central.cloud.core.ServiceHubBase
import net.ibizsys.central.cloud.core.cloudutil.ICloudUtilRuntime
import net.ibizsys.central.cloud.core.cloudutil.client.ICloudDevOpsClient
import net.ibizsys.central.cloud.core.cloudutil.client.ICloudOSSClient
import net.ibizsys.central.cloud.core.sysutil.ISysCloudClientUtilRuntime
import net.ibizsys.central.dataentity.IDataEntityRuntime
import net.ibizsys.central.plugin.cloud.sysutil.SysOSSUtilRuntime
import net.ibizsys.central.plugin.poi.dataentity.dataexport.POIDEDataExportRuntime
import net.ibizsys.central.util.IEntity
import net.ibizsys.central.util.IEntityDTO
import net.ibizsys.model.dataentity.dataexport.IPSDEDataExport
import net.ibizsys.runtime.dataentity.DataEntityRuntimeException
import net.ibizsys.runtime.dataentity.IDataEntityRuntimeBaseContext
import net.ibizsys.runtime.util.IWebContext
import net.ibizsys.runtime.util.WebContext
import org.apache.poi.ss.usermodel.BorderStyle
import org.apache.poi.ss.usermodel.FillPatternType
import org.apache.poi.ss.usermodel.HorizontalAlignment
import org.apache.poi.ss.usermodel.VerticalAlignment
import org.apache.poi.ss.util.CellRangeAddress
import org.apache.poi.xssf.usermodel.XSSFCell
import org.apache.poi.xssf.usermodel.XSSFCellStyle
import org.apache.poi.xssf.usermodel.XSSFColor
import org.apache.poi.xssf.usermodel.XSSFFont
import org.apache.poi.xssf.usermodel.XSSFRow
import org.apache.poi.xssf.usermodel.XSSFSheet
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.docx4j.convert.in.xhtml.XHTMLImporterImpl
import org.docx4j.openpackaging.packages.WordprocessingMLPackage
import org.jsoup.Jsoup
import org.jsoup.helper.W3CDom
import org.springframework.data.domain.Page
import org.springframework.util.ObjectUtils
import org.springframework.util.StringUtils
import net.ibizsys.central.util.ISearchContextDTOimport java.awt.Color
import java.util.stream.Collectors
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream@CompileStatic
public class IdeaDataExportRuntimeEx extends POIDEDataExportRuntime {private static String BREAK_PAGE_STR = "<div style=\"page-break-after: always;\"></div>"private static String TITLE_PAGE_STR = "<h1 id=\"%s\">%s</h1>";private static String IDEA_DOC_TYPE = "idea";private static String DEFAULT_BASE_LB_URI = "http://ibiz-ebsx-allinone:30000";private String base_uri = "";@Overridevoid init(IDataEntityRuntimeBaseContext iDataEntityRuntimeBaseContext, IPSDEDataExport iPSDEDataExport) throws Exception {super.init(iDataEntityRuntimeBaseContext, iPSDEDataExport)base_uri = this.getImportParam("base_uri", DEFAULT_BASE_LB_URI);}@Overrideprotected void onExportStream(Object objData, OutputStream outputStram) throws Throwable {try {ByteArrayOutputStream zipOutputStream = new ByteArrayOutputStream();ZipOutputStream zipStream = new ZipOutputStream(zipOutputStream);if (!(objData instanceof Page)) {if (!(objData instanceof IEntityDTO)) {throw new Exception(String.format("无法识别的数据对象[%s]", objData));}} else {Page page = (Page) objData;Map<String, File> fontInfo = detectChineseFont() as Map<String, File>File fontFile = fontInfo.file as FileString fontFamily = fontInfo.nameif (fontFile == null || !fontFile.exists()) {throw new RuntimeException("未找到可用的中文字体,请手动指定字体路径。")}// 合并输出Map<String, String> mergeContentMap = new HashMap<>();IDataEntityRuntime categoryRuntime = this.getSystemRuntime().getDataEntityRuntime("category")IDataEntityRuntime sectionRuntime = this.getSystemRuntime().getDataEntityRuntime("section")String productName = ""List pages = sortRootPages(page.getContent() as List<IEntity>);// , categoryRuntime, sectionRuntimefor (Object item : pages) {IEntity _default = (IEntity) item;String strId = _default.get("id");String strType = _default.get("type");String strTitle = _default.get("title", "需求标题");productName = _default.get("product_name", "产品名称");strTitle = FileNameSanitizer.sanitizeFileName(strTitle);String strContent = (String) _default.get("description", "");// if(!PAGE_DOC_TYPE.equals(strType)){//     continue;// }ByteArrayOutputStream pdfStream = new ByteArrayOutputStream();ZipEntry entry = null;strContent = String.format(TITLE_PAGE_STR, strId, strTitle) + strContent;mergeContentMap.put(strId, strContent + BREAK_PAGE_STR);try {org.jsoup.nodes.Document doc = buildDoc(fontFamily, strContent);// 执行转换runHtmlConvert(fontFile, fontFamily, doc, pdfStream);entry = new ZipEntry(String.format("%s_%s.pdf", _default.get("identifier"), strTitle));
//                            entry = new ZipEntry(String.format("%s_%s.docx",  _default.get("identifier"),strTitle));} catch (IOException ex) {Throwable e = ex;throw new DataEntityRuntimeException(this.getDataEntityRuntime(), String.format("导出数据发生异常:%s", e.getMessage()));} finally {pdfStream.close();}zipStream.putNextEntry(entry);zipStream.write(pdfStream.toByteArray());zipStream.closeEntry();}// 生成合并pdfif (pages.size() > 1) {ByteArrayOutputStream mergePdfStream = new ByteArrayOutputStream();PdfRendererBuilder builder = new PdfRendererBuilder();String mergeContent = generateToc(mergeContentMap, pages);org.jsoup.nodes.Document doc = buildDoc(fontFamily, mergeContent);// 执行转换runHtmlConvert(fontFile, fontFamily, doc, mergePdfStream);String mergeFileName = String.format("%s_合并.pdf", productName)ZipEntry entry = new ZipEntry(mergeFileName);
//                    ZipEntry entry = new ZipEntry("merge.docx");zipStream.putNextEntry(entry);zipStream.write(mergePdfStream.toByteArray());zipStream.closeEntry();}}zipStream.close();outputStram.write(zipOutputStream.toByteArray());}catch (Exception ex) {Throwable e = ex;throw new DataEntityRuntimeException(this.getDataEntityRuntime(), String.format("导出数据发生异常:%s", e.getMessage()));}}/**** @param out* @return*/private void runHtmlConvert(File fontFile, String fontFamily, org.jsoup.nodes.Document doc, OutputStream out) {PdfRendererBuilder builder = new PdfRendererBuilder();builder.withW3cDocument(new W3CDom().fromJsoup(doc), base_uri);builder.useFont(fontFile, fontFamily);builder.useFastMode();builder.toStream(out);builder.run();
//        //转word
//        WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.createPackage();
//        XHTMLImporterImpl XHTMLImporter = new XHTMLImporterImpl(wordMLPackage);
//        wordMLPackage.getMainDocumentPart().getContent().addAll(XHTMLImporter.convert(new W3CDom().fromJsoup(doc), "http://172.16.240.100:30250"));
//        wordMLPackage.save(out);}/*** 处理html* @param fontFamily* @param strHtml* @return*/private org.jsoup.nodes.Document buildDoc(String fontFamily, String strHtml) {org.jsoup.nodes.Document doc = Jsoup.parse(String.format("<html><style>\n" +".directory-tree ul { list-style-type: none; padding-left: 20px; }\n" +".directory-tree a { text-decoration: none; color: #007BFF;}\n" +".directory-tree a:hover { text-decoration: underline; }\n" +".directory-tree span { color: #333;}\n" +"</style>" +"<body style=\"font-family: '%s'\">%s</body></html>", fontFamily, strHtml));// 处理图片org.jsoup.select.Elements imgs = doc.select("img");for (org.jsoup.nodes.Element img : imgs) {img.attr("style", "width: 100%; height: auto; page-break-inside: avoid; display: block; margin: auto;");// 获取原始src属性String originalSrc = img.attr("src");String newSrc = originalSrc;if (originalSrc.startsWith("/api")) {newSrc = originalSrc.substring(originalSrc.indexOf("/ibizutil"))}img.attr("src", newSrc);}return doc;}private static Map detectChineseFont() {String osName = System.getProperty("os.name").toLowerCase()List<Map> candidates = []if (osName.contains("win")) {candidates.addAll([[name: "宋体", file: new File("C:/Windows/Fonts/simsun.ttc")],[name: "黑体", file: new File("C:/Windows/Fonts/simhei.ttf")],[name: "微软雅黑", file: new File("C:/Windows/Fonts/msyh.ttc")]])} else if (osName.contains("mac")) {candidates.addAll([[name: "苹方", file: new File("/System/Library/Fonts/PingFang.ttc")],[name: "华文细黑", file: new File("/System/Library/Fonts/STHeiti Light.ttc")],[name: "华文黑体", file: new File("/System/Library/Fonts/STHeiti Medium.ttc")]])} else if (osName.contains("nix") || osName.contains("nux") || osName.contains("aix")) {candidates.addAll([[name: "宋体", file: new File("/usr/share/fonts/simsun.ttc")],[name: "黑体", file: new File("/usr/share/fonts/simhei.ttf")],[name: "仿宋", file: new File("/usr/share/fonts/simfang.ttf.ttc")]])}for (Map<String, File> candidate : candidates) {if (candidate.file.exists()) {return candidate}}return [name: null, file: null]}/**** 基于POI解析* @param excelData luckysheet 表格数据*/private void exportLuckySheetXlsxByPOI(String excelData, OutputStream out) {excelData = excelData.replace("&#xA;", "\\r\\n");// 去除luckysheet中 &#xA 的换行ObjectMapper objectMapper = new ObjectMapper();try {JsonNode jsonArray = objectMapper.readTree(excelData);// 创建操作Excel的XSSFWorkbook对象XSSFWorkbook excel = new XSSFWorkbook();for (int sheetIndex = 0; sheetIndex < jsonArray.size(); sheetIndex++) {JsonNode jsonObject = jsonArray.get(sheetIndex);JsonNode celldataObjectList = jsonObject.get("celldata");JsonNode rowObjectList = jsonObject.get("visibledatarow");JsonNode colObjectList = jsonObject.get("visibledatacolumn");JsonNode dataObjectList = jsonObject.get("data");JsonNode mergeObject = jsonObject.get("config").get("merge");// 合并单元格JsonNode columnlenObject = jsonObject.get("config").get("columnlen");// 表格列宽JsonNode rowlenObject = jsonObject.get("config").get("rowlen");// 表格行高JsonNode borderInfoObjectList = jsonObject.get("config").get("borderInfo");// 边框样式// 创建XSSFSheet对象XSSFSheet sheet = excel.createSheet(jsonObject.get("name").asText());// 根据luckysheet创建行列if (rowObjectList != null) {for (int i = 0; i < rowObjectList.size(); i++) {XSSFRow row = sheet.createRow(i);// 创建行try {if (rowlenObject != null) {row.setHeightInPoints(Float.parseFloat(rowlenObject.get(i).asText()));// 行高px值}} catch (Exception e) {row.setHeightInPoints(20f);// 默认行高}if (colObjectList != null) {for (int j = 0; j < colObjectList.size(); j++) {if (columnlenObject != null && columnlenObject.get(j + "").isIntegralNumber()) {sheet.setColumnWidth(j, columnlenObject.get(j + "").asInt() * 42);// 列宽px值}row.createCell(j);// 创建列}}}}// 设置值,样式setCellValue(celldataObjectList, borderInfoObjectList, sheet, excel);}excel.write(out);} catch (IOException ex) {Throwable e = ex;throw new DataEntityRuntimeException(this.getDataEntityRuntime(), String.format("Excel导出异常:%s", e.getMessage()));}}private static void setMergeAndColorByObject(JsonNode jsonNode, XSSFSheet sheet, XSSFCellStyle style) throws IOException {ObjectMapper objectMapper = new ObjectMapper();
//        JsonNode jsonNode = objectMapper.readTree(jsonObjectValue);JsonNode mergeObject = jsonNode.get("mc");if (mergeObject != null) {int r = mergeObject.get("r").asInt();int c = mergeObject.get("c").asInt();if (mergeObject.has("rs") && mergeObject.has("cs")) {int rs = mergeObject.get("rs").asInt();int cs = mergeObject.get("cs").asInt();CellRangeAddress region = new CellRangeAddress(r, r + rs - 1, (short) c, (short) (c + cs - 1));sheet.addMergedRegion(region);}}JsonNode bgNode = jsonNode.get("bg");if (bgNode != null) {int bg = Integer.parseInt(bgNode.asText().replace("#", ""), 16);style.setFillPattern(FillPatternType.SOLID_FOREGROUND);    // 设置填充方案style.setFillForegroundColor(new XSSFColor(new Color(bg), null));  // 设置填充颜色}}private static void setBorder(JsonNode borderInfoObjectList, XSSFWorkbook workbook, XSSFSheet sheet) throws IOException {// 设置边框样式mapMap<Integer, BorderStyle> bordMap = new HashMap<>();bordMap.put(1, BorderStyle.THIN);bordMap.put(2, BorderStyle.HAIR);bordMap.put(3, BorderStyle.DOTTED);bordMap.put(4, BorderStyle.DASHED);bordMap.put(5, BorderStyle.DASH_DOT);bordMap.put(6, BorderStyle.DASH_DOT_DOT);bordMap.put(7, BorderStyle.DOUBLE);bordMap.put(8, BorderStyle.MEDIUM);bordMap.put(9, BorderStyle.MEDIUM_DASHED);bordMap.put(10, BorderStyle.MEDIUM_DASH_DOT);bordMap.put(11, BorderStyle.MEDIUM_DASH_DOT_DOT);bordMap.put(12, BorderStyle.SLANTED_DASH_DOT);bordMap.put(13, BorderStyle.THICK);ObjectMapper objectMapper = new ObjectMapper();
//        JsonNode borderInfoObjectList = objectMapper.readTree(borderInfoJson);// 设置边框for (int i = 0; i < borderInfoObjectList.size(); i++) {JsonNode borderInfoObject = borderInfoObjectList.get(i);if ("cell".equals(borderInfoObject.get("rangeType").asText())) { // 单个单元格JsonNode borderValueObject = borderInfoObject.get("value");JsonNode l = borderValueObject.get("l");JsonNode r = borderValueObject.get("r");JsonNode t = borderValueObject.get("t");JsonNode b = borderValueObject.get("b");int row = borderValueObject.get("row_index").asInt();int col = borderValueObject.get("col_index").asInt();XSSFCell cell = sheet.getRow(row).getCell(col);if (l != null) {cell.getCellStyle().setBorderLeft(bordMap.get(l.get("style").asInt())); // 左边框int bg = Integer.parseInt(l.get("color").asText().replace("#", ""), 16);cell.getCellStyle().setLeftBorderColor(new XSSFColor(new Color(bg), null));// 左边框颜色}if (r != null) {cell.getCellStyle().setBorderRight(bordMap.get(r.get("style").asInt())); // 右边框int bg = Integer.parseInt(r.get("color").asText().replace("#", ""), 16);cell.getCellStyle().setRightBorderColor(new XSSFColor(new Color(bg), null));// 右边框颜色}if (t != null) {cell.getCellStyle().setBorderTop(bordMap.get(t.get("style").asInt())); // 顶部边框int bg = Integer.parseInt(t.get("color").asText().replace("#", ""), 16);cell.getCellStyle().setTopBorderColor(new XSSFColor(new Color(bg), null));// 顶部边框颜色}if (b != null) {cell.getCellStyle().setBorderBottom(bordMap.get(b.get("style").asInt())); // 底部边框int bg = Integer.parseInt(b.get("color").asText().replace("#", ""), 16);cell.getCellStyle().setBottomBorderColor(new XSSFColor(new Color(bg), null));// 底部边框颜色}} else if ("range".equals(borderInfoObject.get("rangeType").asText())) { // 选区int bg_ = Integer.parseInt(borderInfoObject.get("color").asText().replace("#", ""), 16);int style_ = borderInfoObject.get("style").asInt();JsonNode rangObject = borderInfoObject.withArray("range").get(0);JsonNode rowList = rangObject.get("row");JsonNode columnList = rangObject.get("column");for (int row_ = rowList.get(0).asInt(); row_ <= rowList.get(rowList.size() - 1).asInt(); row_++) {for (int col_ = columnList.get(0).asInt(); col_ <= columnList.get(columnList.size() - 1).asInt(); col_++) {XSSFCell cell = sheet.getRow(row_).getCell(col_);cell.getCellStyle().setBorderLeft(bordMap.get(style_)); // 左边框cell.getCellStyle().setLeftBorderColor(new XSSFColor(new Color(bg_), null));// 左边框颜色cell.getCellStyle().setBorderRight(bordMap.get(style_)); // 右边框cell.getCellStyle().setRightBorderColor(new XSSFColor(new Color(bg_), null));// 右边框颜色cell.getCellStyle().setBorderTop(bordMap.get(style_)); // 顶部边框cell.getCellStyle().setTopBorderColor(new XSSFColor(new Color(bg_), null));// 顶部边框颜色cell.getCellStyle().setBorderBottom(bordMap.get(style_)); // 底部边框cell.getCellStyle().setBottomBorderColor(new XSSFColor(new Color(bg_), null));// 底部边框颜色}}}}}private static void setCellValue(JsonNode jsonObjectList, JsonNode borderInfoObjectList, XSSFSheet sheet, XSSFWorkbook workbook) throws IOException {ObjectMapper objectMapper = new ObjectMapper();
//        JsonNode jsonObjectList = objectMapper.readTree(jsonObjectListStr);
//        JsonNode borderInfoObjectList = objectMapper.readTree(borderInfoObjectListStr);// 设置字体大小和颜色Map<Integer, String> fontMap = new HashMap<>();fontMap.put(-1, "Arial");fontMap.put(0, "Times New Roman");fontMap.put(1, "Arial");fontMap.put(2, "Tahoma");fontMap.put(3, "Verdana");fontMap.put(4, "微软雅黑");fontMap.put(5, "宋体");fontMap.put(6, "黑体");fontMap.put(7, "楷体");fontMap.put(8, "仿宋");fontMap.put(9, "新宋体");fontMap.put(10, "华文新魏");fontMap.put(11, "华文行楷");fontMap.put(12, "华文隶书");for (int index = 0; index < jsonObjectList.size(); index++) {XSSFCellStyle style = workbook.createCellStyle();// 样式XSSFFont font = workbook.createFont();// 字体样式JsonNode object = jsonObjectList.get(index);String str_ = object.get("r").asText() + "_" + object.get("c").asText() + "=" + object.get("v").get("v").asText() + "\n";JsonNode jsonObjectValue = object.get("v");String value = "";if (jsonObjectValue != null && jsonObjectValue.get("v") != null) {value = jsonObjectValue.get("v").asText();}int rowIndex = object.get("r").asInt();int colIndex = object.get("c").asInt();XSSFRow row = sheet.getRow(rowIndex);if (row == null) {row = sheet.createRow(rowIndex);}XSSFCell cell = row.getCell(colIndex);if (cell == null) {cell = row.createCell(colIndex);}if (jsonObjectValue != null && jsonObjectValue.get("f") != null) {// 如果有公式,设置公式value = jsonObjectValue.get("f").asText();cell.setCellFormula(value.substring(1, value.length()));// 不需要=符号}// 合并单元格与填充单元格颜色setMergeAndColorByObject(jsonObjectValue, sheet, style);// 填充值cell.setCellValue(value);// 设置垂直水平对齐方式int vt = jsonObjectValue.path("vt").asInt(1);// 垂直对齐	 0 中间、1 上、2下int ht = jsonObjectValue.path("ht").asInt(1);// 0 居中、1 左、2右switch (vt) {case 0:style.setVerticalAlignment(VerticalAlignment.CENTER);break;case 1:style.setVerticalAlignment(VerticalAlignment.TOP);break;case 2:style.setVerticalAlignment(VerticalAlignment.BOTTOM);break;}switch (ht) {case 0:style.setAlignment(HorizontalAlignment.CENTER);break;case 1:style.setAlignment(HorizontalAlignment.LEFT);break;case 2:style.setAlignment(HorizontalAlignment.RIGHT);break;}// 设置字体属性String ff = jsonObjectValue.path("ff").asText("1");
// 0 Times New Roman、 1 Arial、2 Tahoma 、3 Verdana、4 微软雅黑、5 宋体(Song)、6 黑体(ST Heiti)、7 楷体(ST Kaiti)、 8 仿宋(ST FangSong)、9 新宋体(ST Song)、10 华文新魏、11 华文行楷、12 华文隶书int fs = jsonObjectValue.path("fs").asInt(14);// 字体大小int bl = jsonObjectValue.path("bl").asInt(0);// 粗体	0 常规 、 1加粗int it = jsonObjectValue.path("it").asInt(0);// 斜体	0 常规 、 1 斜体String fc = jsonObjectValue.path("fc").asText();// 字体颜色font.setFontName(fontMap.getOrDefault(Integer.valueOf(ff), "Arial"));// 字体名字if (!fc.isEmpty()) {font.setColor(new XSSFColor(new Color(Integer.parseInt(fc.replace("#", ""), 16)), null));}font.setFontHeightInPoints((short) fs);// 字体大小font.setBold(bl == 1);// 粗体显示font.setItalic(it == 1);// 斜体style.setFont(font);style.setWrapText(true);// 设置自动换行cell.setCellStyle(style);}// 设置边框if (borderInfoObjectList != null) {setBorder(borderInfoObjectList, workbook, sheet);}}/*** 在 HTML 开头插入目录页(包含跳转链接)*/private static String generateToc(Map pageContentMap, List pages) {StringBuilder toc = new StringBuilder();toc.append("<div class='directory-tree' style='margin: 40px; font-size: 12pt; line-height: 1.5; '>");toc.append("<h1>目录</h1>");toc.append("<ul>");StringBuffer fullHtml = new StringBuffer();// 构建目录树Map<String, List> treeMap = new HashMap<>();Boolean bFirst = true;for (Object page : pages) {IEntity pageEntity = (IEntity) page;if (IDEA_DOC_TYPE.equals(pageEntity.get("type"))) {String parentId = pageEntity.get("parent_id", "root");treeMap.computeIfAbsent(parentId, { k -> new ArrayList<>() }).add(page);}}// 选拔最顶层节点Set<String> allIds = pages.stream().map({ IEntity pageEntity -> pageEntity.get("id") }).collect(Collectors.toSet()) as Set<String>;Set<String> topIds = pages.stream().filter({ IEntity pageEntity ->String parentId = pageEntity.get("parent_id", "root");// 如果 parent_id 为 null,或者 parent_id 不在所有对象的 ID 列表中,则为顶层节点return "root".equals(parentId) || !allIds.contains(parentId);}).map({ IEntity pageEntity -> pageEntity.get("parent_id", "root") }) // 提取 ID.collect(Collectors.toSet()) as Set<String>;// 遍历树并生成目录项for (String topId : topIds) {buildToc(toc, treeMap, topId, 0, fullHtml, pageContentMap);}toc.append("</ul></div>");toc.append(BREAK_PAGE_STR);// 插入目录页到 HTML 正文之前return toc + fullHtml.toString();}private static void buildToc(StringBuilder toc, Map<String, List> treeMap, String parentId, int level, StringBuffer fullHtml, Map pageContentMap) {List children = treeMap.getOrDefault(parentId, Collections.emptyList());for (Object child : children) {IEntity childEntity = (IEntity) child;String id = childEntity.get("id");String strType = childEntity.get("type");String title = childEntity.get("title", "无标题");toc.append("<li>");for (int i = 0; i < level; i++) {toc.append("&nbsp;&nbsp;&nbsp;&nbsp;"); // 缩进}toc.append("<a href=\"#").append(id).append("\">").append(title).append("</a>");fullHtml.append(pageContentMap.get(id));toc.append("</li>\n");// 递归处理子节点buildToc(toc, treeMap, id, level + 1, fullHtml, pageContentMap);}}/*** 对根节点进行高级排序:* 1. 如果存在 spaceId == id 的 page,放第一位;* 2. type=2 的 page 放在 type=1 前面,内部按 order 排序;* 3. type=1 的 page 按 order 排序。*/private List<IEntity> sortRootPages(List<IEntity> rootPages) {// , IDataEntityRuntime categoryRuntime, IDataEntityRuntime sectionRuntimeList<IEntity> remainingPages = new ArrayList<>(rootPages);List<IEntity> ideaList = new ArrayList<>();List<IEntityDTO> sectionList = new ArrayList<>();List<IEntityDTO> categoryList = new ArrayList<>();for (IEntity page : remainingPages) {page.set("type", "idea")String categoryId = page.get("category_id");String parent_id = categoryId ?: null;page.set("parent_id", parent_id);ideaList.add(page);}// 各组排序Collections.sort(ideaList, { o1, o2 ->((BigDecimal) o1.get("sequence")).compareTo((BigDecimal) o2.get("sequence"))})// 合并结果List<IEntity> sortedList = new ArrayList<>();// sortedList.addAll(sectionList);// sortedList.addAll(categoryList);sortedList.addAll(ideaList);return sortedList;}protected Boolean isExportRoot() {IWebContext iWebContext = this.getUserContext().getWebContext();if (iWebContext != null) {return !StringUtils.hasLength(iWebContext.getParameter("ispkg"));}return true;}class FileNameSanitizer {// 定义非法字符的正则表达式private static final String ILLEGAL_CHARS_REGEX = /[\\\/:*?"<>|\x00-\x1F]/// Windows 保留名称(不区分大小写)private static final Set<String> RESERVED_NAMES = ['CON', 'PRN', 'AUX', 'NUL','COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9','LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'] as Set/*** 清理文件名,替换非法字符* @param fileName 原始文件名* @param replacement 替换用的字符或字符串,默认为 "_"* @return 清理后的文件名*/static String sanitizeFileName(String fileName, String replacement = '_') {if (!fileName) return 'unnamed'  // null 或空字符串处理// 替换非法字符String cleanName = fileName.replaceAll(ILLEGAL_CHARS_REGEX, replacement)// 去除首尾空格和结尾的点(Windows 不允许)cleanName = cleanName.trim()if (cleanName.startsWith('.')) {cleanName = cleanName[1..-1]}if (cleanName.endsWith('.')) {cleanName = cleanName[0..<-1]}// 防止保留名称(如 CON.txt)String baseName = cleanNameint lastDotIndex = cleanName.lastIndexOf('.')if (lastDotIndex != -1) {baseName = cleanName[0..<lastDotIndex]}if (RESERVED_NAMES.contains(baseName.toUpperCase())) {cleanName = "${replacement}${cleanName}"}// 防止全被替换后为空if (!cleanName.trim()) {cleanName = 'unnamed'}return cleanName}}
}
3.3.2 设置插件至数据导出模型

3.4 建立界面行为

UX_PAGEUX_IEDA进行界面行为,处理模式为用户自定义,通过编写自定义js代码调用后台定义的导出能力
在这里插入图片描述
自定义脚本代码

let appDataEntity = await ibiz.hub.getAppDataEntity(action.appDataEntityId,action.appId);
let appDEDataExport = appDataEntity.appDEDataExports?.find(dataExport => {return dataExport.defaultMode === true;
});
if (appDEDataExport) {let url = `spaces/${context.space}/${appDataEntity.deapicodeName2}/exportdata/fetch_export`;let queryParam = { srfexporttag: appDEDataExport.codeName };let params = {page: 0,size: appDEDataExport.maxRowCount ? appDEDataExport.maxRowCount : 1000};if (viewParam && Object.keys(viewParam).length > 0) {Object.assign(params,viewParam);}let app = await ibiz.hub.getAppAsync(action.appId);let res = await app.net.request(url, {method: 'post',responseType: 'blob',params: queryParam,data: params});if (res.status === 200) {let fileName ="页面导出.zip";let blob = new Blob([res.data], {type: 'application/zip'});let elink = document.createElement('a');elink.download = fileName;elink.style.display = 'none';elink.href = URL.createObjectURL(blob);document.body.appendChild(elink);elink.click();URL.revokeObjectURL(elink.href); // 释放URL 对象document.body.removeChild(elink);} else {throw new RuntimeError(ibiz.i18n.t('runtime.uiAction.exportRequestFailed'),);}
} else {throw new RuntimeError(ibiz.i18n.t('runtime.uiAction.noEntityExportsFound'),);
}

其它界面行为请访问建模工具

3.5 建立界面行为组

为了将已经建立的界面行为附加到主系统对于的界面操作菜单上,我们需要建立与主系统同名的界面行为组,加入界面行为
在这里插入图片描述

四、市场及安装

4.1 将插件加入iBizPLM市场

在iBizPLM的市场定义文件(如 projects.yml)中注册此插件。
在这里插入图片描述

4.2 插件安装

在iBizPLM系统“应用市场”进行插件安装
在这里插入图片描述

五、插件效果

5.1 全知识库导出
  1. 进入知识库

  2. 点击"更多"按钮 →"导出页面",您可一键导出全部页面。下载的ZIP文件中,同时提供了每页的独立PDF与一份带书签目录的合并PDF,方便您按需使用。
    全库导出

  3. 全库导出ZIP
    全库导出ZIP包

  4. 导出PDF预览
    页面PDF

5.2 单页面导出
  1. 选中目标页面

  2. 点击"更多操作"→"导出页面"

    单页导出

六、结束语

本文系统性地介绍了iBizPLM知识库导出插件的设计思路与开发要点。通过该插件,用户能够灵活地将在线知识资产转化为离线的、标准化的文档,极大地便利了知识的利用和传播。

该插件的开发充分体现了iBizPLM平台的扩展性:通过建模快速构建前端及后台的逻辑处理复杂业务,最终通过市场机制进行分发和安装。目前,该插件已在Gitee平台开源(仓库地址:https://gitee.com/ibizplm-open/plm-wiki-exchange),并提供在线iBizModeling工具访问。 希望能为需要类似功能的开发者提供一个参考,也欢迎共同参与贡献,完善其功能。

http://www.dtcms.com/a/432685.html

相关文章:

  • 茌平网站建设wordpress定时发布失败处理
  • 做网站都去哪里找模板线下推广图片
  • 网站做收款要什么条件app 外包开发公司
  • 做公司网站图片算是商用吗达州设计公司
  • 网站建设原做网站网站代理赚钱吗
  • 网站网络推广方法wordpress cpu100%
  • 青岛网站建设服务公司网站上的格式用html怎么做
  • 阿里云对象存储做静态网站开发公司员工内部销售激励方案
  • 怎样做网站设计要交税吗济南软件开发外包公司
  • 怎么把网站上传到域名网站建设营业执照如何写
  • 做视频网站犯法吗wordpress怎么安装拖拽编辑软件
  • 如何免费让网站上线网站排名优化软件联系方式
  • BUUCTF [HarekazeCTF2019]baby_rop wp
  • Nestjs框架: 菜单Menu接口功能的开发和设计
  • 做网站放广告邢台市疾控中心
  • 速通ACM省铜第十九天 赋源码(SUMdamental Decomposition)
  • C语言字符函数和字符串函数+内存操作函数
  • 容桂营销网站建设如何做公司简介介绍
  • html5建设摄影网站意义网站建设如何推广
  • ai设计logo免费网站自助建站系统注册
  • 网站开发怎么自学开发app怎么赚钱
  • 网站外部链接合理建设外贸建站费用
  • 网站模板建站公司如何提升网站的流量
  • 湖北中英双语网站建设济南网站怎么做seo
  • 高创园网站建设方案wordpress 搬家 空白
  • 网站地图怎么做XML成都seo优化
  • 科普:使用 apt 或 pip安装软件包前,为何要执行更新update操作
  • 便利的集团网站建设外贸服装网站开发
  • 网站 分析广西城乡建设局和住建局官网
  • 昆明专业网站建设模板合肥建设集团信息网站