|
|
@@ -0,0 +1,634 @@
|
|
|
+package cn.start.tz.module.pressure2.service.excel;
|
|
|
+
|
|
|
+import cn.start.tz.framework.common.pojo.CommonResult;
|
|
|
+import cn.start.tz.module.infra.api.file.FileApi;
|
|
|
+import cn.start.tz.module.pressure2.controller.admin.dynamictb.vo.DynamicTbRespVO;
|
|
|
+import cn.start.tz.module.pressure2.controller.admin.dynamictbcol.vo.DynamicTbColRespVO;
|
|
|
+import cn.start.tz.module.pressure2.controller.admin.dynamictbval.vo.DynamicTBAndColVO;
|
|
|
+import cn.start.tz.module.pressure2.controller.admin.dynamictbval.vo.DynamicTBViewOrAddVO;
|
|
|
+import cn.start.tz.module.pressure2.controller.admin.dynamictbval.vo.DynamicTbValRespVO;
|
|
|
+import cn.start.tz.module.pressure2.service.dynamictb.DynamicTbService;
|
|
|
+import cn.start.tz.module.pressure2.service.dynamictbcol.DynamicTbColService;
|
|
|
+import cn.start.tz.module.pressure2.service.dynamictbval.DynamicTbValService;
|
|
|
+import cn.start.tz.module.pressure2.service.standardfile.StandardTemplateService;
|
|
|
+import cn.start.tz.module.pressure2.util.ImageUtil;
|
|
|
+import cn.start.tz.module.system.api.standard.StandardTemplateApi;
|
|
|
+import cn.start.tz.module.system.api.standard.dto.StandardTemplateRespDTO;
|
|
|
+import com.alibaba.fastjson2.JSON;
|
|
|
+import com.alibaba.fastjson2.JSONArray;
|
|
|
+import com.alibaba.fastjson2.JSONObject;
|
|
|
+import com.grapecity.documents.excel.*;
|
|
|
+import com.grapecity.documents.excel.drawing.ImageType;
|
|
|
+import com.grapecity.documents.excel.template.DataSource.JsonDataSource;
|
|
|
+import jakarta.annotation.Resource;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import java.io.ByteArrayInputStream;
|
|
|
+import java.io.ByteArrayOutputStream;
|
|
|
+import java.util.*;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Excel 处理 Service 实现
|
|
|
+ * 将前端 SpreadJS 编辑 Excel 的功能迁移到后端 GcExcel 处理
|
|
|
+ * 参考 PdfServiceImpl 中的 GcExcel 使用模式
|
|
|
+ */
|
|
|
+@Service
|
|
|
+@Slf4j
|
|
|
+public class ExcelServiceImpl implements ExcelService {
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private StandardTemplateApi standardTemplateApi;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private StandardTemplateService standardTemplateService;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private DynamicTbValService dynamicTbValService;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private DynamicTbColService dynamicTbColService;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private DynamicTbService dynamicTbService;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private FileApi fileApi;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public byte[] processExcel(String templateId, String refId) throws Exception {
|
|
|
+ return processExcel(templateId, refId, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public byte[] processExcel(String templateId, String refId, boolean continuePage) throws Exception {
|
|
|
+ // 1. 获取模板 SJS 字节流
|
|
|
+// CommonResult<StandardTemplateRespDTO> standardTemplate = standardTemplateApi.getStandardTemplate(templateId);
|
|
|
+// String fileUrl = standardTemplate.getCheckedData().getFileUrl();
|
|
|
+ String fileUrl = standardTemplateService.getById(templateId).getFileUrl();
|
|
|
+ byte[] templateBytes = fileApi.getFileByPath(fileUrl).getCheckedData();
|
|
|
+
|
|
|
+ // 2. 获取动态报表数据(实例 + 值列表 + 字段列表)
|
|
|
+ DynamicTBViewOrAddVO queryVO = new DynamicTBViewOrAddVO();
|
|
|
+ queryVO.setTemplateId(templateId);
|
|
|
+ queryVO.setRefId(refId);
|
|
|
+ DynamicTBAndColVO tbData = dynamicTbValService.getDynamicTbInsAndValByRefId(queryVO);
|
|
|
+
|
|
|
+ if (tbData == null || tbData.getDynamicTbInsRespVO() == null) {
|
|
|
+ log.warn("未找到动态报表实例数据, templateId={}, refId={}", templateId, refId);
|
|
|
+ return templateBytes;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 构建数据 Map
|
|
|
+ Map<String, Object> dataMap = buildDataMap(tbData.getDynamicTbValRespVOList());
|
|
|
+
|
|
|
+ // 4. 获取可编辑字段列表
|
|
|
+ List<String> editCols = tbData.getDynamicTbColRespVOList().stream()
|
|
|
+ .filter(i -> Boolean.TRUE.equals(i.getIsEdit()))
|
|
|
+ .map(DynamicTbColRespVO::getColCode)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ // 5. 获取图片字段列表
|
|
|
+ List<String> imgCols = tbData.getDynamicTbColRespVOList().stream()
|
|
|
+ .filter(i -> i.getColValType() != null && i.getColValType() == 4)
|
|
|
+ .map(DynamicTbColRespVO::getColCode)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ // 6. 使用 GcExcel 处理工作簿
|
|
|
+ byte[] processedBytes = processWorkbook(templateBytes, dataMap, imgCols, editCols);
|
|
|
+
|
|
|
+ // 7. 处理续页
|
|
|
+ if (continuePage) {
|
|
|
+ processedBytes = handleContinuationPages(processedBytes, templateId, tbData);
|
|
|
+ }
|
|
|
+
|
|
|
+ return processedBytes;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建数据 Map,将 JSON 字符串解析为实际对象
|
|
|
+ * 确保 GcExcel 模板引擎能正确处理表格绑定
|
|
|
+ */
|
|
|
+ private Map<String, Object> buildDataMap(List<DynamicTbValRespVO> valRespVOList) {
|
|
|
+ Map<String, Object> dataMap = new HashMap<>();
|
|
|
+ if (valRespVOList == null) return dataMap;
|
|
|
+
|
|
|
+ for (DynamicTbValRespVO item : valRespVOList) {
|
|
|
+ String colCode = item.getColCode();
|
|
|
+ String valValue = item.getValValue();
|
|
|
+ if (colCode == null || valValue == null) continue;
|
|
|
+
|
|
|
+ String trimmed = valValue.trim();
|
|
|
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
|
+ try {
|
|
|
+ dataMap.put(colCode, JSON.parse(trimmed));
|
|
|
+ } catch (Exception e) {
|
|
|
+ dataMap.put(colCode, valValue);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ dataMap.put(colCode, valValue);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return dataMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 核心处理逻辑:使用 GcExcel 处理工作簿
|
|
|
+ */
|
|
|
+ private byte[] processWorkbook(byte[] templateBytes, Map<String, Object> dataMap,
|
|
|
+ List<String> imgCols, List<String> editCols) throws Exception {
|
|
|
+ WorkbookOptions workbookOptions = new WorkbookOptions();
|
|
|
+ workbookOptions.setPixelBasedColumnWidth(true);
|
|
|
+
|
|
|
+ Workbook workbook = new Workbook(workbookOptions);
|
|
|
+ // SJS 格式不使用 XlsxOpenOptions,直接以 SJS 方式打开
|
|
|
+ workbook.open(new ByteArrayInputStream(templateBytes), OpenFileFormat.Sjs);
|
|
|
+
|
|
|
+ for (int i = 0; i < workbook.getWorksheets().getCount(); i++) {
|
|
|
+ IWorksheet worksheet = workbook.getWorksheets().get(i);
|
|
|
+
|
|
|
+ // -- 设置数据源前收集表格合并规则 --
|
|
|
+ Map<ITable, List<int[]>> tableMergeInfo = collectTableMergeInfo(worksheet);
|
|
|
+
|
|
|
+ // -- 设置数据源 --
|
|
|
+ worksheet.setDataSource(new JsonDataSource(JSON.toJSONString(dataMap)));
|
|
|
+
|
|
|
+ // -- 表格行样式同步 --
|
|
|
+ copyTableRowStyles(worksheet, tableMergeInfo);
|
|
|
+
|
|
|
+ // -- 处理单元格图片 --
|
|
|
+ handleCellImages(worksheet);
|
|
|
+
|
|
|
+ // -- 处理图片字段 (colValType == 4) --
|
|
|
+ handleImageCols(worksheet, imgCols, dataMap);
|
|
|
+
|
|
|
+ // -- 处理浮动图片 --
|
|
|
+ handleFloatingImages(worksheet, dataMap);
|
|
|
+
|
|
|
+ // -- 处理复选框 --
|
|
|
+ handleCheckboxCells(worksheet);
|
|
|
+
|
|
|
+ // -- 设置可编辑字段高亮 --
|
|
|
+ highlightEditableCells(worksheet, editCols);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存为 SJS 格式返回
|
|
|
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
|
|
+ workbook.save(outputStream, SaveFileFormat.Sjs);
|
|
|
+ return outputStream.toByteArray();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理单元格中的图片:将以 .jpg/.png 结尾的值替换为图片
|
|
|
+ * 参考 PdfServiceImpl.fullPdf() 中的图片处理逻辑
|
|
|
+ */
|
|
|
+ private void handleCellImages(IWorksheet worksheet) {
|
|
|
+ IRange usedRange = worksheet.getUsedRange();
|
|
|
+ if (usedRange == null) return;
|
|
|
+
|
|
|
+ for (int row = 0; row < usedRange.getRowCount(); row++) {
|
|
|
+ for (int col = 0; col < usedRange.getColumnCount(); col++) {
|
|
|
+ IRange cell = worksheet.getRange(row, col);
|
|
|
+ if (cell.getBindingPath() == null || cell.getValue() == null) continue;
|
|
|
+
|
|
|
+ Object valueObj = cell.getValue();
|
|
|
+ String value = valueObj instanceof String ? (String) valueObj : String.valueOf(valueObj);
|
|
|
+
|
|
|
+ if (!value.endsWith(".png") && !value.endsWith(".jpg")) continue;
|
|
|
+
|
|
|
+ // 单个图片
|
|
|
+ if (!value.contains(",")) {
|
|
|
+ try {
|
|
|
+ byte[] imgBytes = fileApi.getFileByPath(value).getCheckedData();
|
|
|
+ cell.setValue(null);
|
|
|
+ IRange mergeArea = cell.getMergeArea();
|
|
|
+ mergeArea.setBackgroundImage(imgBytes);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("设置单元格背景图片失败: path={}, error={}", value, e.getMessage());
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 多张图片,横向合并
|
|
|
+ String[] split = value.split(",");
|
|
|
+ byte[][] imgBytesArr = new byte[split.length][];
|
|
|
+ boolean allSuccess = true;
|
|
|
+ for (int k = 0; k < split.length; k++) {
|
|
|
+ try {
|
|
|
+ imgBytesArr[k] = fileApi.getFileByPath(split[k]).getCheckedData();
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("获取图片失败: path={}", split[k]);
|
|
|
+ allSuccess = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (allSuccess && imgBytesArr.length > 0) {
|
|
|
+ cell.setValue(null);
|
|
|
+ IRange mergeArea = cell.getMergeArea();
|
|
|
+ byte[] mergedImg = ImageUtil.mergeImages(imgBytesArr);
|
|
|
+ mergeArea.setBackgroundImage(mergedImg);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理图片字段类型 (colValType == 4):单元格中存储 JSON 数组 [{url, x, y, width, height}]
|
|
|
+ * 参考前端 editReport 中 imgCol 分支的处理
|
|
|
+ */
|
|
|
+ private void handleImageCols(IWorksheet worksheet, List<String> imgCols, Map<String, Object> dataMap) {
|
|
|
+ if (imgCols == null || imgCols.isEmpty()) return;
|
|
|
+ if (dataMap == null) return;
|
|
|
+
|
|
|
+ IRange usedRange = worksheet.getUsedRange();
|
|
|
+ if (usedRange == null) return;
|
|
|
+
|
|
|
+ for (int row = 0; row < usedRange.getRowCount(); row++) {
|
|
|
+ for (int col = 0; col < usedRange.getColumnCount(); col++) {
|
|
|
+ String bindingPath = worksheet.getRange(row, col).getBindingPath();
|
|
|
+ if (bindingPath == null || !imgCols.contains(bindingPath)) continue;
|
|
|
+
|
|
|
+ Object valueObj = dataMap.get(bindingPath);
|
|
|
+ if (valueObj == null) continue;
|
|
|
+
|
|
|
+ JSONArray imgArr = toJSONArray(valueObj);
|
|
|
+ if (imgArr == null || imgArr.isEmpty()) continue;
|
|
|
+
|
|
|
+ // 清空单元格文本值
|
|
|
+ worksheet.getRange(row, col).setValue(null);
|
|
|
+
|
|
|
+ for (int k = 0; k < imgArr.size(); k++) {
|
|
|
+ try {
|
|
|
+ JSONObject imgItem = imgArr.getJSONObject(k);
|
|
|
+ String url = imgItem.getString("url");
|
|
|
+ if (url == null) continue;
|
|
|
+
|
|
|
+ byte[] imgBytes = fileApi.getFileByPath(url).getCheckedData();
|
|
|
+ ByteArrayInputStream imgStream = new ByteArrayInputStream(imgBytes);
|
|
|
+ worksheet.getShapes().addPictureInPixel(
|
|
|
+ JSON.toJSONString(imgItem),
|
|
|
+ imgStream, ImageType.JPG,
|
|
|
+ imgItem.getDoubleValue("x"),
|
|
|
+ imgItem.getDoubleValue("y"),
|
|
|
+ imgItem.getDoubleValue("width"),
|
|
|
+ imgItem.getDoubleValue("height")
|
|
|
+ );
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("添加图片形状失败: index={}", k, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理浮动图片:从数据源的 Illustration 字段解析并添加
|
|
|
+ * 参考前端 editReport 中的浮动图片处理 + PdfServiceImpl.fullPdf() 中的 illustration 处理
|
|
|
+ */
|
|
|
+ private void handleFloatingImages(IWorksheet worksheet, Map<String, Object> dataMap) {
|
|
|
+ if (dataMap == null) return;
|
|
|
+
|
|
|
+ Object illustrationObj = dataMap.get("Illustration");
|
|
|
+ if (illustrationObj == null) return;
|
|
|
+
|
|
|
+ JSONArray jsonArray = toJSONArray(illustrationObj);
|
|
|
+ if (jsonArray == null || jsonArray.isEmpty()) return;
|
|
|
+
|
|
|
+ List<JSONObject> remainingItems = new ArrayList<>();
|
|
|
+ for (int k = 0; k < jsonArray.size(); k++) {
|
|
|
+ JSONObject jsonObject = jsonArray.getJSONObject(k);
|
|
|
+ String sheetName = jsonObject.getString("sheet");
|
|
|
+ if (worksheet.getName().equals(sheetName)) {
|
|
|
+ try {
|
|
|
+ String url = jsonObject.getString("url");
|
|
|
+ byte[] imgBytes = fileApi.getFileByPath(url).getCheckedData();
|
|
|
+ ByteArrayInputStream imgStream = new ByteArrayInputStream(imgBytes);
|
|
|
+ worksheet.getShapes().addPictureInPixel(
|
|
|
+ url,
|
|
|
+ imgStream, ImageType.JPG,
|
|
|
+ jsonObject.getDoubleValue("x"),
|
|
|
+ jsonObject.getDoubleValue("y"),
|
|
|
+ jsonObject.getDoubleValue("width"),
|
|
|
+ jsonObject.getDoubleValue("height")
|
|
|
+ );
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("添加浮动图片失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ remainingItems.add(jsonObject);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新数据源,移除已处理的浮动图片
|
|
|
+ if (!remainingItems.isEmpty()) {
|
|
|
+ dataMap.put("Illustration", JSON.toJSONString(remainingItems));
|
|
|
+ } else {
|
|
|
+ dataMap.remove("Illustration");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理复选框单元格:清空非 "true" 的值
|
|
|
+ */
|
|
|
+ private void handleCheckboxCells(IWorksheet worksheet) {
|
|
|
+ IRange usedRange = worksheet.getUsedRange();
|
|
|
+ if (usedRange == null) return;
|
|
|
+
|
|
|
+ for (int row = 0; row < usedRange.getRowCount(); row++) {
|
|
|
+ for (int col = 0; col < usedRange.getColumnCount(); col++) {
|
|
|
+ IRange cell = worksheet.getRange(row, col);
|
|
|
+ if (cell.getCellType() instanceof CheckBoxCellType) {
|
|
|
+ Object val = cell.getValue();
|
|
|
+ if (val == null) continue;
|
|
|
+ String strVal = val instanceof String ? (String) val : String.valueOf(val);
|
|
|
+ if (!"true".equals(strVal)) {
|
|
|
+ cell.setValue(null);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 对齐前端 reportUtil.editReport 中的可编辑字段处理:
|
|
|
+ * ① 全部单元格设为不可编辑
|
|
|
+ * ② 可编辑字段解锁
|
|
|
+ * ③ 用一条条件格式规则批量设置 #FFF895 黄色背景
|
|
|
+ * 打印 PDF 时可批量移除规则,不对最终输出产生影响
|
|
|
+ */
|
|
|
+ private void highlightEditableCells(IWorksheet worksheet, List<String> editCols) {
|
|
|
+ if (editCols == null || editCols.isEmpty()) return;
|
|
|
+
|
|
|
+ IRange usedRange = worksheet.getUsedRange();
|
|
|
+ if (usedRange == null) return;
|
|
|
+ worksheet.protect();
|
|
|
+ // ① 全部单元格设为不可编辑(对齐前端 cellRange.allowEditInCell(false))
|
|
|
+ usedRange.setLocked(true);
|
|
|
+
|
|
|
+ // ② 遍历:解锁可编辑字段,同时收集地址
|
|
|
+ List<String> editableAddresses = new ArrayList<>();
|
|
|
+ for (int row = 0; row < usedRange.getRowCount(); row++) {
|
|
|
+ for (int col = 0; col < usedRange.getColumnCount(); col++) {
|
|
|
+ IRange cell = worksheet.getRange(row, col);
|
|
|
+ String bindingPath = cell.getBindingPath();
|
|
|
+ if (bindingPath != null && editCols.contains(bindingPath)) {
|
|
|
+ // 解锁可编辑(对齐前端 sheet.getCell(i,j).allowEditInCell(true))
|
|
|
+ cell.setLocked(false);
|
|
|
+ editableAddresses.add(cell.getAddress());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ③ 批量设置条件格式(对齐前端 cfs.addSpecificTextRule(greaterThanOrEqualsTo, null, style, arr))
|
|
|
+ if (!editableAddresses.isEmpty()) {
|
|
|
+ String unionAddress = String.join(",", editableAddresses);
|
|
|
+ IFormatCondition condition = (IFormatCondition) worksheet.getRange(unionAddress)
|
|
|
+ .getFormatConditions().add(FormatConditionType.Expression, FormatConditionOperator.None, "=TRUE", null);
|
|
|
+ // #FFF895 = RGB(255, 248, 149)
|
|
|
+ condition.getInterior().setColor(Color.FromArgb(255, 255, 248, 149));
|
|
|
+ System.out.println(condition.getFormula1());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 表格行样式同步 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 在 setDataSource 前收集所有表格第一行数据行的合并单元格规则
|
|
|
+ * 参考 PdfServiceImpl.collectTableMergeInfo()
|
|
|
+ */
|
|
|
+ private Map<ITable, List<int[]>> collectTableMergeInfo(IWorksheet worksheet) {
|
|
|
+ Map<ITable, List<int[]>> result = new HashMap<>();
|
|
|
+ if (worksheet.getTables() == null) return result;
|
|
|
+
|
|
|
+ for (int t = 0; t < worksheet.getTables().getCount(); t++) {
|
|
|
+ ITable table = worksheet.getTables().get(t);
|
|
|
+ IRange tableRange = table.getRange();
|
|
|
+ int firstDataRow = tableRange.getRow() + 1;
|
|
|
+ int firstDataCol = tableRange.getColumn();
|
|
|
+ int colCount = tableRange.getColumnCount();
|
|
|
+
|
|
|
+ List<int[]> mergeRanges = new ArrayList<>();
|
|
|
+ try {
|
|
|
+ for (int c = firstDataCol; c < firstDataCol + colCount; c++) {
|
|
|
+ IRange cell = worksheet.getRange(firstDataRow, c);
|
|
|
+ IRange mergeArea = cell.getMergeArea();
|
|
|
+ if (mergeArea != null && mergeArea.getColumnCount() > 1) {
|
|
|
+ mergeRanges.add(new int[]{mergeArea.getColumn(), mergeArea.getColumnCount()});
|
|
|
+ c = mergeArea.getColumn() + mergeArea.getColumnCount() - 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ }
|
|
|
+ result.put(table, mergeRanges);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将第一行数据行的样式、行高、合并规则复制到所有自动扩展的数据行
|
|
|
+ * 参考 PdfServiceImpl.copyTableRowStyles()
|
|
|
+ */
|
|
|
+ private void copyTableRowStyles(IWorksheet worksheet, Map<ITable, List<int[]>> tableMergeInfo) {
|
|
|
+ if (worksheet.getTables() == null) return;
|
|
|
+
|
|
|
+ for (int t = 0; t < worksheet.getTables().getCount(); t++) {
|
|
|
+ ITable table = worksheet.getTables().get(t);
|
|
|
+ IRange tableRange = table.getRange();
|
|
|
+ int rowCount = tableRange.getRowCount();
|
|
|
+ int colCount = tableRange.getColumnCount();
|
|
|
+ if (rowCount <= 1) continue;
|
|
|
+
|
|
|
+ int firstDataRow = tableRange.getRow();
|
|
|
+ int firstDataCol = tableRange.getColumn();
|
|
|
+
|
|
|
+ List<int[]> mergeColRanges = tableMergeInfo.getOrDefault(table, new ArrayList<>());
|
|
|
+
|
|
|
+ double firstRowHeight = worksheet.getRange(firstDataRow, firstDataCol).getRowHeight();
|
|
|
+ IRange sourceRow = worksheet.getRange(firstDataRow, firstDataCol, 1, colCount);
|
|
|
+
|
|
|
+ for (int r = 1; r < rowCount; r++) {
|
|
|
+ int targetRow = tableRange.getRow() + r;
|
|
|
+ IRange targetRowRange = worksheet.getRange(targetRow, firstDataCol, 1, colCount);
|
|
|
+ PasteOption pasteOption = new PasteOption();
|
|
|
+ pasteOption.setPasteType(EnumSet.of(PasteType.Formats));
|
|
|
+ sourceRow.copy(targetRowRange, pasteOption);
|
|
|
+ worksheet.getRange(targetRow, firstDataCol).setRowHeight(firstRowHeight);
|
|
|
+
|
|
|
+ for (int[] mergeRange : mergeColRanges) {
|
|
|
+ try {
|
|
|
+ worksheet.getRange(targetRow, mergeRange[0], 1, mergeRange[1]).merge();
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置表格范围自动换行
|
|
|
+ tableRange.setWrapText(true);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 续页处理 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理续页:检查 copyConfig 配置,处理隐藏空页和自动生成续页
|
|
|
+ * 参考前端 SpreadViewer.vue 中的 hiddenPage() 和 handleGenerate()
|
|
|
+ */
|
|
|
+ private byte[] handleContinuationPages(byte[] sjsBytes, String templateId, DynamicTBAndColVO tbData) {
|
|
|
+ try {
|
|
|
+ // 获取模板的 copyConfig(注意:DynamicTbRespVO 中的 copyConfig 是 fastjson1 JSONObject)
|
|
|
+ DynamicTbRespVO templateInfo = dynamicTbService.getDynamicTb(templateId);
|
|
|
+ Object rawConfig = templateInfo != null ? templateInfo.getCopyConfig() : null;
|
|
|
+ if (rawConfig == null) return sjsBytes;
|
|
|
+
|
|
|
+ // 兼容 fastjson1 JSONObject → fastjson2 JSONObject,通过序列化中转
|
|
|
+ JSONObject copyConfig = rawConfig instanceof JSONObject
|
|
|
+ ? (JSONObject) rawConfig
|
|
|
+ : JSON.parseObject(rawConfig.toString());
|
|
|
+
|
|
|
+ Boolean isContinuePage = copyConfig.getBoolean("isContinuePage");
|
|
|
+ if (!Boolean.TRUE.equals(isContinuePage)) return sjsBytes;
|
|
|
+
|
|
|
+ String sheetName = copyConfig.getString("sheetName");
|
|
|
+ if (sheetName == null) return sjsBytes;
|
|
|
+
|
|
|
+ JSONObject copyRange = copyConfig.getJSONObject("copyRange");
|
|
|
+ if (copyRange == null) return sjsBytes;
|
|
|
+
|
|
|
+ // 解析坐标
|
|
|
+ String[] topLeft = copyRange.getString("topLeft").split(",");
|
|
|
+ String[] topRight = copyRange.getString("topRight").split(",");
|
|
|
+ String[] bottomLeft = copyRange.getString("bottomLeft").split(",");
|
|
|
+
|
|
|
+ int col = Integer.parseInt(topLeft[0]);
|
|
|
+ int row = Integer.parseInt(topLeft[1]);
|
|
|
+ int colCount = Integer.parseInt(topRight[0]) - col;
|
|
|
+ int rowCount = Integer.parseInt(bottomLeft[1]) - row;
|
|
|
+
|
|
|
+ WorkbookOptions workbookOptions = new WorkbookOptions();
|
|
|
+ workbookOptions.setPixelBasedColumnWidth(true);
|
|
|
+ Workbook workbook = new Workbook(workbookOptions);
|
|
|
+ workbook.open(new ByteArrayInputStream(sjsBytes), OpenFileFormat.Sjs);
|
|
|
+
|
|
|
+ IWorksheet targetSheet = null;
|
|
|
+ for (int i = 0; i < workbook.getWorksheets().getCount(); i++) {
|
|
|
+ if (workbook.getWorksheets().get(i).getName().equals(sheetName)) {
|
|
|
+ targetSheet = workbook.getWorksheets().get(i);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (targetSheet == null) return sjsBytes;
|
|
|
+
|
|
|
+ Boolean hidden = copyConfig.getBoolean("hidden");
|
|
|
+ if (Boolean.TRUE.equals(hidden)) {
|
|
|
+ // 判断第一行是否全部为空,是则隐藏
|
|
|
+ boolean isEmpty = true;
|
|
|
+ for (int i = 0; i <= colCount; i++) {
|
|
|
+ Object cellValue = targetSheet.getRange(row, col + i).getValue();
|
|
|
+ if (cellValue != null && !cellValue.toString().isEmpty()) {
|
|
|
+ isEmpty = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (isEmpty) {
|
|
|
+ targetSheet.setVisible(Visibility.Hidden);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取动态字段列表,用于创建续页绑定字段
|
|
|
+ List<String> existingColCodes = tbData.getDynamicTbColRespVOList().stream()
|
|
|
+ .map(DynamicTbColRespVO::getColCode)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ List<String> newColCodes = new ArrayList<>();
|
|
|
+
|
|
|
+ int sheetNum = 2;
|
|
|
+
|
|
|
+ // 判断最后一行是否非空,自动生成续页
|
|
|
+ boolean isNotEmpty = false;
|
|
|
+ for (int i = 0; i <= colCount; i++) {
|
|
|
+ Object cellValue = targetSheet.getRange(row + rowCount, col + i).getValue();
|
|
|
+ if (cellValue != null && !cellValue.toString().isEmpty()) {
|
|
|
+ isNotEmpty = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isNotEmpty) {
|
|
|
+ // 生成续页:复制 sheet 并调整绑定路径
|
|
|
+ generateContinuedSheet(workbook, targetSheet, sheetName, sheetNum,
|
|
|
+ col, row, colCount, rowCount, existingColCodes, newColCodes);
|
|
|
+ sheetNum++;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果有新字段编码,批量创建
|
|
|
+ if (!newColCodes.isEmpty()) {
|
|
|
+ try {
|
|
|
+ dynamicTbColService.createDynamicTbColByCodes(newColCodes, templateId);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("批量创建续页字段失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
|
|
+ workbook.save(outputStream, SaveFileFormat.Sjs);
|
|
|
+ return outputStream.toByteArray();
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 续页处理失败不影响主流程,返回原始 SJS
|
|
|
+ log.warn("续页处理失败,回退返回原始 SJS: templateId={}", templateId, e);
|
|
|
+ return sjsBytes;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成续页工作表
|
|
|
+ * 参考前端 handleGenerate() 中的逻辑
|
|
|
+ * 使用 GcExcel 的 toJson/fromJson 实现工作簿内 sheet 复制
|
|
|
+ */
|
|
|
+ private void generateContinuedSheet(Workbook workbook, IWorksheet sourceSheet,
|
|
|
+ String sheetName, int sheetNum,
|
|
|
+ int col, int row, int colCount, int rowCount,
|
|
|
+ List<String> existingColCodes, List<String> newColCodes) {
|
|
|
+ // 通过序列化/反序列化方式复制工作表
|
|
|
+ String sheetJson = sourceSheet.toJson();
|
|
|
+ IWorksheet newSheet = workbook.getWorksheets().add();
|
|
|
+ newSheet.fromJson(sheetJson);
|
|
|
+ String newSheetName = sheetName + " (" + sheetNum + ")";
|
|
|
+ newSheet.setName(newSheetName);
|
|
|
+
|
|
|
+ // 调整绑定路径:将数字部分递增
|
|
|
+ for (int i = 0; i <= colCount; i++) {
|
|
|
+ for (int j = 0; j <= rowCount; j++) {
|
|
|
+ String bindingPath = sourceSheet.getRange(row + j, col + i).getBindingPath();
|
|
|
+ if (bindingPath != null && bindingPath.contains("_")) {
|
|
|
+ String prefix = bindingPath.substring(0, bindingPath.lastIndexOf("_") + 1);
|
|
|
+ String numStr = bindingPath.substring(bindingPath.lastIndexOf("_") + 1);
|
|
|
+ try {
|
|
|
+ int num = Integer.parseInt(numStr);
|
|
|
+ int newNum = num + (rowCount + 1) * (sheetNum - 1);
|
|
|
+ String newPath = prefix + newNum;
|
|
|
+ newSheet.getRange(row + j, col + i).setBindingPath(newPath);
|
|
|
+ if (!existingColCodes.contains(newPath) && !newColCodes.contains(newPath)) {
|
|
|
+ newColCodes.add(newPath);
|
|
|
+ }
|
|
|
+ } catch (NumberFormatException ignored) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 工具方法 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将对象安全转换为 JSONArray
|
|
|
+ */
|
|
|
+ private JSONArray toJSONArray(Object obj) {
|
|
|
+ if (obj instanceof JSONArray arr) return arr;
|
|
|
+ if (obj instanceof String str && str.trim().startsWith("[")) {
|
|
|
+ try {
|
|
|
+ return JSON.parseArray(str);
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|