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

前端打印功能(vue +springboot)

后端

  • 后端
    • 依赖
    • 生成pdf的方法
    • pdf转图片
    • 使用(用的打印模版是带参数的 ,参数是aaa)
    • 总结
  • 前端
    • 页面
  • 效果

后端

依赖

依赖 一个是用模版生成对应的pdf,一个是用来将pdf转成图片需要的

      <!--打印的--><dependency><groupId>net.sf.jasperreports</groupId><artifactId>jasperreports</artifactId></dependency><dependency><groupId>org.apache.pdfbox</groupId><artifactId>pdfbox</artifactId><version>3.0.0</version></dependency>

所需的文件资源 jasper是模版,然后font是pdf转图片的时候需要的字体
在这里插入图片描述
由于有资源 所以pom配置俩包下面不允许压缩

    <build><resources><resource><targetPath>${project.build.directory}/classes</targetPath><directory>src/main/resources</directory><filtering>true</filtering><excludes><exclude>**/*.jasper</exclude><exclude>**/*.jrxml</exclude><exclude>**/*.TTF</exclude></excludes></resource><resource><targetPath>${project.build.directory}/classes</targetPath><directory>src/main/resources</directory><filtering>false</filtering><includes><include>**/*.jasper</include><include>**/*.jrxml</include><include>**/*.TTF</include></includes></resource></resources></build>

生成pdf的方法

用到的实体类

package com.xueyi.common.core.utils.print.dto;import lombok.Data;@Data
public class PdfInfoDto {private Integer height;private Integer width;private byte[] pdfBytes;
}

生成pdf的方法

package com.xueyi.common.core.utils.print;import com.xueyi.common.core.utils.print.dto.PdfInfoDto;
import net.sf.jasperreports.engine.*;
import net.sf.jasperreports.engine.export.JRPdfExporter;
import net.sf.jasperreports.engine.util.JRLoader;
import net.sf.jasperreports.export.SimpleExporterInput;
import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput;
import net.sf.jasperreports.export.SimplePdfExporterConfiguration;
import org.springframework.core.io.ClassPathResource;import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;public class PrintUtil {public static PdfInfoDto printPDF(String modelName, HashMap hashMap) throws IOException, JRException {// 获取模板路径ClassPathResource classPathResource = new ClassPathResource("/jasper/"+modelName+".jasper");// 获取模板输入流InputStream inputStream = classPathResource.getInputStream();// 读取模板JasperReport report = (JasperReport) JRLoader.loadObject(inputStream);//数据库数据填充报表JasperPrint jprint = JasperFillManager.fillReport(report, hashMap,new JREmptyDataSource());DefaultJasperReportsContext defaultJasperReportsContext=DefaultJasperReportsContext.getInstance();
//        defaultJasperReportsContext.setProperty("net.sf.jasperreports.fonts.STSong-Light", "path/to/STSong-Light.ttf");//创建导出对象JRPdfExporter exporter = new JRPdfExporter(defaultJasperReportsContext);//设置要导出的流exporter.setExporterInput(new SimpleExporterInput(jprint));ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(byteArrayOutputStream));SimplePdfExporterConfiguration configuration = new SimplePdfExporterConfiguration();configuration.setCreatingBatchModeBookmarks(true);exporter.setConfiguration(configuration);// 导出pdfexporter.exportReport();byte[] bytes=byteArrayOutputStream.toByteArray();byteArrayOutputStream.close();PdfInfoDto pdfInfo=new PdfInfoDto();pdfInfo.setPdfBytes(bytes);pdfInfo.setHeight(jprint.getPageHeight());pdfInfo.setWidth(jprint.getPageWidth());return pdfInfo;}}

pdf转图片

用到的实体类

package com.xueyi.common.core.utils.print.dto;import lombok.Data;@Data
public class ImageInfoDto {private Integer height;private Integer width;private byte[] imageBytes;
}
package com.xueyi.common.core.utils.print;import com.xueyi.common.core.utils.print.dto.ImageInfoDto;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;public class Pdf2PngUtil {public static List<ImageInfoDto> pdf2png(byte[] bytes) throws IOException {ArrayList<ImageInfoDto> list=new ArrayList();PDDocument doc=Loader.loadPDF(bytes);PDFRenderer renderer = new PDFRenderer(doc);int pageCount = doc.getNumberOfPages();for (int i = 0; i < pageCount; i++) {// dpi为144,越高越清晰,转换越慢BufferedImage image = renderer.renderImageWithDPI(i, 144); // Windows native DPIByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();ImageIO.createImageOutputStream(byteArrayOutputStream);ImageIO.write(image, "png", byteArrayOutputStream);ImageInfoDto imageInfo=new ImageInfoDto();imageInfo.setHeight(image.getHeight());imageInfo.setWidth(image.getWidth());imageInfo.setImageBytes(byteArrayOutputStream.toByteArray());list.add(imageInfo);byteArrayOutputStream.close();}doc.close();return  list;}
}

使用(用的打印模版是带参数的 ,参数是aaa)

返回类

@Data
public class PrintDto {@Serialprivate static final long serialVersionUID = 1L;private String jasperName;private HashMap hashMap;private List<ImageInfoDto> imageInfoDtoList;private PdfInfoDto pdfInfoDto;
}

使用

    @PostMapping("/print")public AjaxResult print(@RequestBody TransSchedulerQuery transSchedulerQuery) throws IOException, JRException {PrintDto printDto=new PrintDto();printDto.setJasperName(transSchedulerQuery.getJasperName());HashMap hashMap=new HashMap();hashMap.put("aaa","你看得到吗?");printDto.setPdfInfoDto(PrintUtil.printPDF(transSchedulerQuery.getJasperName(),hashMap));printDto.setImageInfoDtoList(Pdf2PngUtil.pdf2png(printDto.getPdfInfoDto().getPdfBytes()));return  AjaxResult.success(printDto);}

总结

后端的思路其实很简单,就是用带参数的打印模版,然后把对应参数送进去生成pdf,由于前端需要图片和pdf两种,所以又把生成的pdf转图片生成了一下图片的list.然后不管事pdf和图片,都是直接把文件本身传递回去了,没有存到本地,用url的方法.直接把文件的byte数组传递到前端了(一般序列化的方式就是base64,所以json序列化的时候自动转好了的前端接到的是字符串形式的)

前端

前端其实很简单,就是把得到的文件信息拿到,转成blob形式,然后预览一下.唯一的问题就是拿到的字符串需要进行转码,从base64变回byte数组.因为要有预览的效果,所以一直尝试前端pdf转换然后要自定义打印啥的(Lodop),用了很多无用的代码,后续定下来打印就调用浏览器的打印啥的,那个打印按钮有打印前和打印后的回调函数的,已经满足需求了.

export function base64ToByteArray(base64String: string): Uint8Array {const binaryString = atob(base64String);const length = binaryString.length;const bytes = new Uint8Array(length);for (let i = 0; i < length; i++) {bytes[i] = binaryString.charCodeAt(i);}return bytes;
}

页面

<template><BasicModal  v-bind="$attrs" :width="ModalWidthEnum.COMMON" @register="registerModal" :canFullscreen="false" :title="variable.title" :height="400"   :okText="t('public.button.print')"   @ok="handleSubmit" ><div style="margin-top: 5px;text-align: center;"><span> {{t("public.describe.all")}}{{variable.imagesUrlList.length}}{{t("public.describe.page")}}{{t("public.describe.clickToTurnOver")}}</span></div><iframe ref="pdfFrameRef" :src="variable.pdfUrl" style="display: none"></iframe><Row ref="rowRef">
<!--      <Col :span="1" >-->
<!--        <div style="height: 100%;">-->
<!--          <CaretLeftOutlined style="position: absolute;top: 45%"/>-->
<!--        </div>-->
<!--      </Col>--><Col :span="24"><div style="border: black 1px solid;" ><Image :src="variable.imagesUrlList[0]" width="100%" :preview="variable.preview" @click="variable.preview = true"/><div style="display: none"><Image.PreviewGroup :preview="{ visible:variable.preview, onVisibleChange: vis => (variable.preview = vis) }"><Image v-for="(item) in variable.imagesUrlList"  :src="item" width="100%" /></Image.PreviewGroup></div></div></Col>
<!--      <Col :span="1" >-->
<!--        <div style="height: 100%;">-->
<!--          <CaretRightOutlined style="position: absolute;top: 45%"/>-->
<!--        </div>--><!--      </Col>--></Row></BasicModal>
</template><script setup lang="ts">
import {reactive, ref} from 'vue';
import {useMessage} from '@/hooks/web/useMessage';
import {BasicModal, useModalInner} from '@/components/Modal';
import {useI18n} from "@/hooks/web/useI18n";
import {ModalWidthEnum} from "@/enums";
import {Image,Row,Col,} from 'ant-design-vue';
// 定义国际化
const { t } = useI18n();
const emit = defineEmits(['success']);
const {createMessage} = useMessage();
const isUpdate = ref(true);
import {base64ToByteArray} from "@/utils/commonFunctions/commonFunctions";
/** 标题初始化 */
const variable = reactive<any>({ids: [],fatherParam:{},title:"",pdfUrl:null,pdfBlob:null,imagesUrlList:[],currentIndex:0,currentHeight:0,preview:false,afterPrintFunction:null
});const pdfFrameRef=ref()
const rowRef=ref()const [registerModal, {setModalProps, closeModal,changeOkLoading,changeLoading}] = useModalInner(async (data) => {variable.fatherParam=JSON.parse(JSON.stringify(data.fatherParam))variable.afterPrintFunction=data.afterPrintFunctionvariable.title=data.fatherParam.titlevariable.imagesUrlList=[]variable.currentIndex=0variable.preview=false//有图片的情况if (data.fatherParam.data.imageInfoDtoList?.length>0){variable.currentHeight=variable.fatherParam.data.imageInfoDtoList[0].heightvariable.pdfBlob = new Blob([base64ToByteArray(variable.fatherParam.data?.pdfInfoDto?.pdfBytes)], { type: 'application/pdf'});variable.pdfUrl= window.URL.createObjectURL(variable.pdfBlob)variable.fatherParam.data.imageInfoDtoList.forEach(item=>{const blob1 = new Blob([base64ToByteArray(item.imageBytes)], { type: 'image/png'});variable.imagesUrlList.push(window.URL.createObjectURL(blob1))})}});// const getPrintDevice = () => {
//   var loop = getLodop(); // 创建一个LODOP对象
//   let counter = loop.GET_PRINTER_COUNT(); // 获取打印机个数
//   //初始化下printNameList打印数组
//   variable.printDeviceList = [];
//   for (let i = 0; i < counter; i++) {
//     //将打印机存入printList数组中
//     variable.printDeviceList.push({name:loop.GET_PRINTER_NAME(i),value:i});
//   }
//
//   //获取默认打印机并设置
//   var defaultName =loop.GET_PRINTER_NAME(-1);
//   variable.printDeviceList.forEach(item=>{
//     if(item.name==defaultName){
//       variable.printDevice=item.value
//     }
//   })
// }// const doPrint = () => {
//   var LODOP = getLodop(); // 创建一个LODOP对象
//   // LODOP.ADD_PRINT_IMAGE(0, 0, "<img>", "EMF", variable.pdfUrl);
//   LODOP.PRINT_INIT("打印控件功能演示_Lodop功能_按网址打印");
//   let base64="data:image/png;base64,"+variable.fatherParam.data.imageListByte[0]
//   LODOP.ADD_PRINT_IMAGE(0,0,"100%","100%",base64);
//   console.log(base64)
//   LODOP.PRINT();
// }// // 创建 LODOP 实例
// function getLodop() {
//   let LODOP;
//   if (window.LODOP) {
//     LODOP = window.LODOP;
//   } else if (window.parent.LODOP) {
//     LODOP = window.parent.LODOP;
//   } else {
//     LODOP = (function () {
//       var LODOP = null;
//       var s = document.createElement('script');
//       s.src = '/LODOPfuncs.js';
//       s.id = 'LODOPfuncs';
//       s.type = 'text/javascript';
//       document.getElementsByTagName('head')[0].appendChild(s);
//       document.onpropertychange = function (e) {
//         if (e.propertyName === 'LODOP') {
//           LODOP = e.srcElement.LODOP;
//         }
//       };
//       return LODOP;
//     })();
//   }
//   return LODOP;
// }/** 提交按钮 */
async function handleSubmit() {pdfFrameRef.value.contentWindow.print();window.onafterprint = afterPrint;
}/*** 浏览器打印的回调*/
const afterPrint = async () =>  {variable.afterPrintFunction()
}</script>

效果

在这里插入图片描述

相关文章:

前端打印功能(vue +springboot)

后端 后端依赖生成pdf的方法pdf转图片使用(用的打印模版是带参数的 ,参数是aaa)总结 前端页面 效果 后端 依赖 依赖 一个是用模版生成对应的pdf,一个是用来将pdf转成图片需要的 <!--打印的--><dependency><groupId>net.sf.jasperreports</groupId>&l…...

中间件有哪些分类?

中间件的分类 中间件是位于操作系统和应用程序之间的软件&#xff0c;它提供了一系列服务来简化分布式系统中的应用程序开发和集成。中间件可以根据其功能和用途被分为不同的类别。以下是中间件的一些主要分类&#xff1a; 1. 通信处理&#xff08;消息&#xff09;中间件&am…...

开始新征程__10.13

好久没有更新 csdn 了&#xff0c;身边的人都说 csdn 水&#xff0c;但是在我看来&#xff0c;它在我大一这一年里对我的帮助很大&#xff0c;最近上账号看看&#xff0c;看见了网友评论&#xff0c;哈哈&#xff0c;决定以后还是继续更新&#xff0c;分享自己的学习心得。...

SAP 联合创始人谈Home Office

软件公司 SAP 的家庭办公室规定继续引发激烈争论&#xff0c;其联合创始人哈索-普拉特纳&#xff08;Hasso Plattner&#xff09;对此也有明确看法。 沃尔多夫--年初&#xff0c;SAP 首席执行官克里斯蒂安-克莱因&#xff08;Christian Klein&#xff09;向员工宣誓 "努力…...

基于Jenkins+K8S构建DevOps自动化运维管理平台

目录 1.k8s助力DevOps在企业落地实践 1.1 传统方式部署项目为什么发布慢&#xff0c;效率低&#xff1f; 1.2 上线一个功能&#xff0c;有多少时间被浪费了&#xff1f; 1.3 如何解决发布慢&#xff0c;效率低的问题呢&#xff1f; 1.5 什么是DevOps&#xff1f; 1.5.1 敏…...

【OpenCV】(一)—— 安装opencv环境

【OpenCV】&#xff08;一&#xff09;—— 安装opencv环境 OpenCV&#xff08;Open Source Computer Vision Library&#xff09;是一个开源的计算机视觉和机器学习软件库。OpenCV 是用 C 编写的&#xff0c;但它也有 Python、Java 和 MATLAB 接口&#xff0c;并支持 Windows…...

MybatisPlus操作符和运算值

好久没有更新了&#xff0c;这次更新一个当前端需要对运算符和运算值都需要前端传递给后端&#xff0c;动态拼接运算条件时的处理方法。 1、踩雷 查询年龄 >20&#xff0c;其中>前端下拉框选择&#xff0c;20值前端下拉框选择 1&#xff09;用户表&#xff1a; CREAT…...

Index-1.9B模型部署教程

一、介绍 Index-1.9B 系列是 Index 系列型号的轻量级版本&#xff0c;包含以下型号&#xff1a; Index-1.9B 基础&#xff1a;具有 19 亿个非嵌入参数的基础模型&#xff0c;在 2.8T 主要为中文和英文的语料上进行预训练&#xff0c;在多个评测基准上与同级别模型相比领先。I…...

C语言 | Leetcode C语言题解之第468题验证IP地址

题目&#xff1a; 题解&#xff1a; char * validIPAddress(char * queryIP) {int len strlen(queryIP);if (strchr(queryIP, .)) {// IPv4int last -1;for (int i 0; i < 4; i) {int cur -1;if (i 3) {cur len;} else {char * p strchr(queryIP last 1, .);if (p…...

Qt自定义一个圆角对话框

如何得到一个圆角对话框&#xff1f; 步骤&#xff1a; 1、继承自QDiaglog 2、去掉系统自带的边框 3、设置背景透明,不设置4个角会有多余的部分出现颜色 4、对话框内部添加1个QWidget&#xff0c;给这个widget设置圆角&#xff0c;并添加到布局中让他充满对话框 5、后续对…...

C++ 中的自旋锁应用:SpinLockManual、SpinLockGuard 和 SpinLockTryGuard

在多线程编程中,同步机制是确保线程安全的关键。自旋锁(Spin Lock)是一种常见的同步机制,它通过忙等待(busy-waiting)的方式来实现线程间的互斥访问。在 C++ 中,我们可以使用 std::atomic_flag 来实现自旋锁。本文将介绍如何使用 SpinLockManual、SpinLockGuard 和 Spin…...

计算机网络 tcp和udp

目录 一、TCP 建立连接-TCP 三次握手 1&#xff09; 什么是半连接队列和全连接队列&#xff1f; 2&#xff09; 为什么要三次握手? 3&#xff09; 三次握手过程中可以携带数据吗&#xff1f; 断开连接-TCP 四次挥手 1&#xff09; 为什么要四次挥手&#xff1f; 2&…...

React(一) 认识React、熟悉类组件、JSX书写规范、嵌入变量表达式、绑定属性

文章目录 一、初始React1. React的基本认识2. Hello案例2.1 三个依赖2.2 渲染页面2.3 hello案例完整代码 二、类组件1. 封装类组件2. 组件里的数据3. 组件里的函数 (重点)4. 案例练习(1) 展示电影列表 三、JSX语法1. 认识JSX2. JSX书写规范及注释3. JSX嵌入变量作为子元素4. JS…...

计算机网络 2024 11 10

计算机网络 - 知乎计算机网络&#xff08;四&#xff09;—— 网络层&#xff08;1、2&#xff09;&#xff1a;网络层概述、网络层提供的两种服务_以下属于网络层范畴的是a透明传输比特流b媒体接入控制c ip地址d-CSDN博客 湖科大教书匠的个人空间-湖科大教书匠个人主页-哔哩哔…...

King3399(ubuntu文件系统)风扇驱动

该文章仅供参考&#xff0c;编写人不对任何实验设备、人员及测量结果负责&#xff01;&#xff01;&#xff01; 0 引言 文章主要介绍King3399&#xff08;ubuntu文件系统&#xff09;风扇控制&#xff08;GPIO&#xff09;&#xff0c;涉及king-rk3399.dts设备树修改&#x…...

前端开发笔记--html 黑马程序员2

文章目录 前端常用标签一、标题标签二、段落标签和换行标签和水平线标签三、文本格式化标签![请添加图片描述](https://i-blog.csdnimg.cn/direct/87583fa23fe04229b016912051f3fc45.png)四、盒子标签五、图像标签六、连接标签七、注释和特殊字符 八、表格标签的基本使用九、列…...

django(二):定义第一个函数及url介绍

1.定义index函数 """ django里的第一个函数必须是request,不写会报错 """def index(request):return HttpResponse("Hello, world. Youre at the index of djangoProject.")注意&#xff01; ①.index函数里的形参必须为request ②.r…...

66 消息队列

66 消息队列 基础概念 参考资料&#xff1a;消息队列MQ快速入门&#xff08;概念、RPC、MQ实质思路、队列介绍、队列对比、应用场景&#xff09; 消息队列就是一个使用队列来通信的组件&#xff1b;为什么需要消息队列&#xff1f; 在实际的商业项目中&#xff0c;它这么做肯…...

【系统分析师】-案例篇-信息系统安全

1、信息系统的安全威胁 来自于物理环境、通信链路、网络系统、操作系统、应用系统以及管理等多个方面。 物理安全威胁是指对系统所用设备的威胁&#xff0c;如自然灾害、电源故障、数据库故障和设备被盗等造成数据丢失或信息泄漏。 通信链路安全威胁是指在传输线路上安装窃…...

基于极光优化算法(Polar Lights Optimization, PLO)的多无人机协同三维路径规划(提供MATLAB代码)

一、极光优化算法介绍 极光优化算法&#xff08;Polar Lights Optimization, PLO&#xff09;是2024年提出的一种新型的元启发式优化算法&#xff0c;它从极光这一自然现象中汲取灵感。极光是由太阳风中的带电粒子在地球磁场的作用下&#xff0c;与地球大气层中的气体分子碰撞…...

云计算——弹性云计算器(ECS)

弹性云服务器&#xff1a;ECS 概述 云计算重构了ICT系统&#xff0c;云计算平台厂商推出使得厂家能够主要关注应用管理而非平台管理的云平台&#xff0c;包含如下主要概念。 ECS&#xff08;Elastic Cloud Server&#xff09;&#xff1a;即弹性云服务器&#xff0c;是云计算…...

uni-app学习笔记二十二---使用vite.config.js全局导入常用依赖

在前面的练习中&#xff0c;每个页面需要使用ref&#xff0c;onShow等生命周期钩子函数时都需要像下面这样导入 import {onMounted, ref} from "vue" 如果不想每个页面都导入&#xff0c;需要使用node.js命令npm安装unplugin-auto-import npm install unplugin-au…...

STM32+rt-thread判断是否联网

一、根据NETDEV_FLAG_INTERNET_UP位判断 static bool is_conncected(void) {struct netdev *dev RT_NULL;dev netdev_get_first_by_flags(NETDEV_FLAG_INTERNET_UP);if (dev RT_NULL){printf("wait netdev internet up...");return false;}else{printf("loc…...

Java - Mysql数据类型对应

Mysql数据类型java数据类型备注整型INT/INTEGERint / java.lang.Integer–BIGINTlong/java.lang.Long–––浮点型FLOATfloat/java.lang.FloatDOUBLEdouble/java.lang.Double–DECIMAL/NUMERICjava.math.BigDecimal字符串型CHARjava.lang.String固定长度字符串VARCHARjava.lang…...

【CSS position 属性】static、relative、fixed、absolute 、sticky详细介绍,多层嵌套定位示例

文章目录 ★ position 的五种类型及基本用法 ★ 一、position 属性概述 二、position 的五种类型详解(初学者版) 1. static(默认值) 2. relative(相对定位) 3. absolute(绝对定位) 4. fixed(固定定位) 5. sticky(粘性定位) 三、定位元素的层级关系(z-i…...

ffmpeg(四):滤镜命令

FFmpeg 的滤镜命令是用于音视频处理中的强大工具&#xff0c;可以完成剪裁、缩放、加水印、调色、合成、旋转、模糊、叠加字幕等复杂的操作。其核心语法格式一般如下&#xff1a; ffmpeg -i input.mp4 -vf "滤镜参数" output.mp4或者带音频滤镜&#xff1a; ffmpeg…...

【单片机期末】单片机系统设计

主要内容&#xff1a;系统状态机&#xff0c;系统时基&#xff0c;系统需求分析&#xff0c;系统构建&#xff0c;系统状态流图 一、题目要求 二、绘制系统状态流图 题目&#xff1a;根据上述描述绘制系统状态流图&#xff0c;注明状态转移条件及方向。 三、利用定时器产生时…...

微服务商城-商品微服务

数据表 CREATE TABLE product (id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 商品id,cateid smallint(6) UNSIGNED NOT NULL DEFAULT 0 COMMENT 类别Id,name varchar(100) NOT NULL DEFAULT COMMENT 商品名称,subtitle varchar(200) NOT NULL DEFAULT COMMENT 商…...

vue3+vite项目中使用.env文件环境变量方法

vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量&#xff0c;这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...

Spring AI与Spring Modulith核心技术解析

Spring AI核心架构解析 Spring AI&#xff08;https://spring.io/projects/spring-ai&#xff09;作为Spring生态中的AI集成框架&#xff0c;其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似&#xff0c;但特别为多语…...