导入导出支持国际化语言分隔符

需求

  1. 导出 Excel 时,所有整数、小数字段需要加千分位分隔符显示
    例如:10000 导出成 10,000。
  2. 只影响“导出的显示效果”,不改变原始数据的语义。

实现思路

通过修改“ Excel 默认导出模版”的平台逻辑,将整型字段模版定义为文本类型,并自定义导出扩展,将所有整型字段的数据根据国际化配置进行分割。

代码示例

  1. 自定义导出扩展,分割整型字段
    注意 所有已有的导出扩展必须由修改继承类ExcelExportSameQueryPageTemplate<Object> --> GlobalExportExt<Object>
    否则,已有的导出扩展生成的Excel将无法正常导出整型字段。
package pro.shushi.pamirs.top.core.temp.exports;

import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.file.api.context.ExcelDefinitionContext;
import pro.shushi.pamirs.file.api.entity.EasyExcelBlockDefinition;
import pro.shushi.pamirs.file.api.entity.EasyExcelCellDefinition;
import pro.shushi.pamirs.file.api.entity.EasyExcelSheetDefinition;
import pro.shushi.pamirs.file.api.extpoint.ExcelExportFetchDataExtPoint;
import pro.shushi.pamirs.file.api.extpoint.impl.ExcelExportSameQueryPageTemplate;
import pro.shushi.pamirs.file.api.model.ExcelExportTask;
import pro.shushi.pamirs.meta.annotation.Ext;
import pro.shushi.pamirs.meta.annotation.ExtPoint;
import pro.shushi.pamirs.meta.api.dto.config.ModelFieldConfig;
import pro.shushi.pamirs.meta.api.session.PamirsSession;
import pro.shushi.pamirs.meta.enmu.TtypeEnum;
import pro.shushi.pamirs.meta.util.FieldUtils;
import pro.shushi.pamirs.resource.api.model.ResourceLang;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

@Component
@Ext(ExcelExportTask.class)
public class GlobalExportExt<T> extends ExcelExportSameQueryPageTemplate<T> implements ExcelExportFetchDataExtPoint {

    /**
     * 缓存:modelModel -> (fieldCode -> ttype)
     * ttype 是 ModelFieldConfig.getTtype() 的值
     */
    private static final ConcurrentMap<String, ConcurrentMap<String, String>> MODEL_FIELD_TTYPE_CACHE =
            new ConcurrentHashMap<>();

    @ExtPoint.Implement(priority = 1)
    @Override
    public List<Object> fetchExportData(ExcelExportTask exportTask, ExcelDefinitionContext context) {
        List<Object> results = super.fetchExportData(exportTask, context);
        if (CollectionUtils.isEmpty(results)) {
            return results;
        }
        return dataFormat(context, results);
    }

    public static List<Object> dataFormat(ExcelDefinitionContext context, List<Object> results) {
        List<Map<String, Object>> exportList = new ArrayList<>();
        ResourceLang resourceLang = new ResourceLang().setCode(PamirsSession.getLang()).queryOne();
        if (resourceLang == null) {
            return results;
        }
        // 小数分隔符
        String decimalPoint = resourceLang.getDecimalPoint();
        // 整数分隔符
        String thousandsSep = resourceLang.getThousandsSep();
        // 数字分组规则(每组多少位,比如 "3")
        String groupingRule = resourceLang.getGroupingRule();

        // 解析 groupSize,只做一次
        int groupSize = 3;
        if (groupingRule != null && groupingRule.matches("\\d+")) {
            try {
                groupSize = Integer.parseInt(groupingRule);
            } catch (NumberFormatException ignore) {
            }
        }
        boolean needGroup = groupSize > 0 && thousandsSep != null && !thousandsSep.isEmpty();

        // 判断是否需要小数点替换
        String dp = (decimalPoint == null || decimalPoint.isEmpty()) ? "." : decimalPoint;
        boolean needDecimalReplace = !".".equals(dp);

        EasyExcelSheetDefinition sheetDefinition = context.getSheetList().get(0);
        EasyExcelBlockDefinition blockDefinition = sheetDefinition.getBlockDefinitions().get(0);
        String modelModel = blockDefinition.getBindingModel();

        // 记录Excel中整数字段编码
        List<String> integerField = new ArrayList<>();
        // 记录Excel中小数字段编码
        List<String> decimalField = new ArrayList<>();

        Map<String, EasyExcelCellDefinition> fieldCells = blockDefinition.getFieldCells();
        for (String key : fieldCells.keySet()) {
            String ttype = getFieldTtype(modelModel, key);
            if (ttype == null) {
                continue;
            }
            if (TtypeEnum.INTEGER.value().equals(ttype)) {
                integerField.add(key);
            } else if (TtypeEnum.FLOAT.value().equals(ttype)) {
                decimalField.add(key);
            }
        }

        if (integerField.isEmpty() && decimalField.isEmpty()) {
            return results;
        }
        Object block = results.get(0);
        if (block instanceof List) {
            List<?> dataList = (List<?>) block;
            for (Object row : dataList) {
                // 处理整数字段:加千分位
                if (needGroup) {
                    for (String field : integerField) {
                        Object value = FieldUtils.getFieldValue(row, field);
                        if (value != null) {
                            String formatted = formatInteger(value, groupSize, thousandsSep);
                            FieldUtils.setFieldValue(row, field, formatted);
                        }
                    }
                }
                // 处理小数字段:只替换小数点
                if (needDecimalReplace) {
                    for (String field : decimalField) {
                        Object value = FieldUtils.getFieldValue(row, field);
                        if (value != null) {
                            String formatted = formatDecimal(value, dp);
                            FieldUtils.setFieldValue(row, field, formatted);
                        }
                    }
                }
            }
        }

        return results;
    }

    /**
     * 整数格式化:按照 groupSize 与 thousandsSep 插入分隔符
     */
    public static String formatInteger(Object value, int groupSize, String thousandsSep) {
        String numStr;
        if (value instanceof Integer || value instanceof Long || value instanceof Short) {
            numStr = String.valueOf(value);
        } else if (value instanceof BigDecimal || value instanceof Double || value instanceof Float) {
            numStr = new BigDecimal(value.toString()).toPlainString();
        } else {
            numStr = value.toString().trim();
        }
        boolean negative = false;
        if (numStr.startsWith("-")) {
            negative = true;
            numStr = numStr.substring(1);
        } else if (numStr.startsWith("+")) {
            numStr = numStr.substring(1);
        }

        // 去掉已有的分隔符(防止重复格式化)
        numStr = numStr.replace(",", "").replace(" ", "");

        // 非纯数字直接返回原始字符串
        if (!numStr.matches("\\d+")) {
            return (negative ? "-" : "") + numStr;
        }

        if (groupSize <= 0 || thousandsSep == null || thousandsSep.isEmpty()) {
            return (negative ? "-" : "") + numStr;
        }

        StringBuilder sb = new StringBuilder();
        int len = numStr.length();
        int firstGroupLen = len % groupSize;
        if (firstGroupLen == 0) {
            firstGroupLen = groupSize;
        }
        sb.append(numStr, 0, firstGroupLen);
        for (int i = firstGroupLen; i < len; i += groupSize) {
            sb.append(thousandsSep).append(numStr, i, Math.min(i + groupSize, len));
        }

        return (negative ? "-" : "") + sb.toString();
    }

    /**
     * 小数格式化
     */
    public static String formatDecimal(Object value, String dp) {
        String numStr;

        if (value instanceof BigDecimal || value instanceof Double || value instanceof Float) {
            numStr = new BigDecimal(value.toString()).toPlainString();
        } else {
            numStr = value.toString().trim();
        }

        if (dp == null || ".".equals(dp)) {
            return numStr;
        }
        return numStr.replace(".", dp);
    }

    /**
     * 从缓存中获取字段 ttype,没有则查询 PamirsSession 并写入缓存
     */
    public static String getFieldTtype(String modelModel, String fieldCode) {
        ConcurrentMap<String, String> fieldCache =
                MODEL_FIELD_TTYPE_CACHE.computeIfAbsent(modelModel, m -> new ConcurrentHashMap<>());

        return fieldCache.computeIfAbsent(fieldCode, f -> {
            ModelFieldConfig modelField = PamirsSession.getContext().getModelField(modelModel, f);
            if (modelField == null) {
                return null;
            }
            return modelField.getTtype();
        });
    }

}
  1. 修改平台逻辑,整型字段模版定义为文本类型
    启动工程同包同类名覆盖平台文件。
    pro.shushi.pamirs.file.api.init.FileLifecycleCompletedInit
package pro.shushi.pamirs.file.api.init;

import com.alibaba.fastjson.JSON;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.boot.base.model.View;
import pro.shushi.pamirs.boot.common.api.command.AppLifecycleCommand;
import pro.shushi.pamirs.boot.common.api.init.LifecycleCompletedInit;
import pro.shushi.pamirs.core.common.InitializationUtil;
import pro.shushi.pamirs.core.common.LifecycleExecutorHelper;
import pro.shushi.pamirs.core.common.cache.MemoryIterableSearchCache;
import pro.shushi.pamirs.core.common.xstream.TreeNodeXStream;
import pro.shushi.pamirs.core.common.xstream.XMLNodeContent;
import pro.shushi.pamirs.file.api.FileModule;
import pro.shushi.pamirs.file.api.builder.BlockDefinitionBuilder;
import pro.shushi.pamirs.file.api.builder.HeaderDefinitionBuilder;
import pro.shushi.pamirs.file.api.builder.WorkbookDefinitionBuilder;
import pro.shushi.pamirs.file.api.config.FileConstant;
import pro.shushi.pamirs.file.api.config.FileProperties;
import pro.shushi.pamirs.file.api.enmu.*;
import pro.shushi.pamirs.file.api.model.ExcelWorkbookDefinition;
import pro.shushi.pamirs.file.api.service.ExcelFileService;
import pro.shushi.pamirs.file.api.util.ExcelHelper;
import pro.shushi.pamirs.framework.common.config.AsyncTaskExecutorConfiguration;
import pro.shushi.pamirs.framework.common.entry.TreeNode;
import pro.shushi.pamirs.framework.connectors.data.sql.Pops;
import pro.shushi.pamirs.framework.gateways.util.BooleanHelper;
import pro.shushi.pamirs.meta.annotation.fun.extern.Slf4j;
import pro.shushi.pamirs.meta.api.Models;
import pro.shushi.pamirs.meta.api.dto.config.ModelConfig;
import pro.shushi.pamirs.meta.api.dto.config.ModelFieldConfig;
import pro.shushi.pamirs.meta.api.session.PamirsSession;
import pro.shushi.pamirs.meta.api.session.RequestContext;
import pro.shushi.pamirs.meta.common.constants.CharacterConstants;
import pro.shushi.pamirs.meta.constant.FieldConstants;
import pro.shushi.pamirs.meta.domain.model.DataDictionary;
import pro.shushi.pamirs.meta.domain.model.DataDictionaryItem;
import pro.shushi.pamirs.meta.domain.model.ModelDefinition;
import pro.shushi.pamirs.meta.domain.module.ModuleDefinition;
import pro.shushi.pamirs.meta.enmu.*;

import java.util.*;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;

@Slf4j
@Component
@Order
public class FileLifecycleCompletedInit implements LifecycleCompletedInit {

    private static final String DEFAULT_EXPORT_TEMPLATE_DISPLAY_NAME = "默认导出模板";
    private static final String DEFAULT_IMPORT_TEMPLATE_DISPLAY_NAME = "默认导入模板";

    private static final String DEFAULT_EXPORT_TEMPLATE_SUFFIX = "导出";
    private static final String DEFAULT_IMPORT_TEMPLATE_SUFFIX = "导入";

    private static final String CONFIG_KEY = "config";

    private static final String CONFIG_EXCLUDED_ACTIONS_KEY = "excluded-actions";

    private static final String ACTION_KEY = "action";

    private static final String VIEW_KEY = "view";

    private static final String ACTION_REFS_KEY = "refs";

    private static final String FIELD_KEY = "field";

    private static final String FIELDS_KEY = "fields";

    private static final String FORM_KEY = "form";

    private static final String TEMPLATE_KEY = "template";

    private static final String SLOT_KEY = "slot";

    private static final String PACK_KEY = "pack";

    private static final String TABLE_KEY = "table";

    private static final String DATA_KEY = "data";

    private static final String TYPE_KEY = "type";

    private static final String WIDGET_KEY = "widget";

    private static final String NAME_KEY = "name";

    private static final String FIELD_LABEL_KEY = "label";

    private static final String FIELD_INVISIBLE_KEY = "invisible";

    private static final String EXPORT_DIALOG_KEY = "$$internal_GotoListExportDialog";

    private static final String EXPORT_DIALOG_KEY_V3 = "internal_GotoListExportDialog";

    private static final String DEFAULT_RELATION_LABEL_SPLIT = "-";

    @Autowired
    private ExcelFileService excelFileService;

    @Autowired
    private FileProperties fileProperties;

    @Autowired(required = false)
    @Qualifier(AsyncTaskExecutorConfiguration.FIXED_THREAD_POOL_EXECUTOR)
    private Executor globalFixedThreadPoolExecutor;

    @Override
    public void process(AppLifecycleCommand command, List<ModuleDefinition> installModules, List<ModuleDefinition> upgradeModules, List<ModuleDefinition> reloadModules) {
        log.info("Automatically create import/export system templates: {}", fileProperties.getAutoCreateTemplate());
        if (fileProperties.getAutoCreateTemplate()) {
            InitializationUtil.lifecycleCompletedInit(installModules, upgradeModules, Collections.emptyList(), (lifecycle, module) -> LifecycleExecutorHelper.execute(globalFixedThreadPoolExecutor, this::initDefaultTemplateByTableView), FileModule.MODULE_MODULE);
        }
    }

    private void initDefaultTemplateByTableView() {
        List<ExcelWorkbookDefinition> existImportExportWorkbookDefinitionList = getExistWorkbookDefinitionList(ExcelTemplateTypeEnum.IMPORT_EXPORT);
        // 默认导出模板(Table)
        List<ExcelWorkbookDefinition> existExportWorkbookDefinitionList = getExistWorkbookDefinitionList(ExcelTemplateTypeEnum.EXPORT);
        existExportWorkbookDefinitionList.addAll(existImportExportWorkbookDefinitionList);
        List<ExcelWorkbookDefinition> exportWorkbookDefinitionList = getEffectiveList(existExportWorkbookDefinitionList, ViewTypeEnum.TABLE);

        // 默认导入模板(Form)
        List<ExcelWorkbookDefinition> existImportWorkbookDefinitionList = getExistWorkbookDefinitionList(ExcelTemplateTypeEnum.IMPORT);
        existImportWorkbookDefinitionList.addAll(existImportExportWorkbookDefinitionList);
        List<ExcelWorkbookDefinition> importWorkbookDefinitionList = getEffectiveList(existImportWorkbookDefinitionList, ViewTypeEnum.FORM);

        log.info("Initialization system templates. import template: {}, export template: {}", exportWorkbookDefinitionList.size(), importWorkbookDefinitionList.size());
        refreshWorkbookDefinition(exportWorkbookDefinitionList);
        refreshWorkbookDefinition(importWorkbookDefinitionList);
    }

    private List<ExcelWorkbookDefinition> getExistWorkbookDefinitionList(ExcelTemplateTypeEnum type) {
        List<ExcelWorkbookDefinition> workbookDefinitions = Models.data().queryListByWrapper(Pops.<ExcelWorkbookDefinition>lambdaQuery().from(ExcelWorkbookDefinition.MODEL_MODEL).eq(ExcelWorkbookDefinition::getType, type).setBatchSize(200));
        if (CollectionUtils.isNotEmpty(workbookDefinitions)) {
            workbookDefinitions = Models.data().listFieldQuery(workbookDefinitions, ExcelWorkbookDefinition::getLocations);
        }
        return workbookDefinitions;
    }

    private List<ExcelWorkbookDefinition> getEffectiveList(List<ExcelWorkbookDefinition> existWorkbookDefinitionList, ViewTypeEnum viewType) {
        List<View> allViewList = Models.data().queryListByWrapper(Pops.<View>lambdaQuery().from(View.MODEL_MODEL).select(View::getModel, View::getName, View::getTitle, View::getTemplate).eq(View::getType, viewType).eq(View::getActive, ActiveEnum.ACTIVE.value()).orderByAsc(View::getPriority).setBatchSize(200));
        List<View> distinctViewList = allViewList.stream().collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(View::getModel))), ArrayList::new));
        TreeNodeXStream xs = new TreeNodeXStream();
        Set<String> repeatSet = new HashSet<>();
        Map<ExcelTemplateSourceEnum, List<ExcelWorkbookDefinition>> resultMap = new HashMap<>(existWorkbookDefinitionList.size());
        for (ExcelWorkbookDefinition item : existWorkbookDefinitionList) {
            resultMap.computeIfAbsent(item.getTemplateSource(), k -> new ArrayList<>()).add(item);
            if (!ExcelTemplateSourceEnum.SYSTEM.equals(item.getTemplateSource())) {
                repeatSet.add(item.getModel());
            }
        }
        List<ExcelWorkbookDefinition> createOrUpdateList = new ArrayList<>();
        MemoryIterableSearchCache<String, ExcelWorkbookDefinition> cache = new MemoryIterableSearchCache<>(resultMap.get(ExcelTemplateSourceEnum.SYSTEM), this::keyGenerator);
        for (View view : distinctViewList) {
            String model = view.getModel();
            if (!repeatSet.contains(model)) {
                ExcelWorkbookDefinition workbookDefinition;
                try {
                    workbookDefinition = createOrUpdateExcelTemplate(xs, view);
                } catch (Exception e) {
                    if (log.isErrorEnabled()) {
                        log.error("Excel模板生成失败 model: {}", model, e);
                    }
                    continue;
                }
                if (workbookDefinition != null) {
                    cache.compute(keyGenerator(workbookDefinition), (k, v) -> v);
                    createOrUpdateList.add(workbookDefinition);
                }
            }
        }
        cache.fill();
        List<ExcelWorkbookDefinition> deleteList = new ArrayList<>(cache.getNotComputedCache().values());
        if (!deleteList.isEmpty()) {
            Models.data().deleteByPks(deleteList);
            for (ExcelWorkbookDefinition item : deleteList) {
                String originKey = keyGenerator(item);
                for (int i = 0; i < existWorkbookDefinitionList.size(); i++) {
                    String targetKey = keyGenerator(existWorkbookDefinitionList.get(i));
                    if (originKey.equals(targetKey)) {
                        existWorkbookDefinitionList.remove(i);
                        break;
                    }
                }
            }
        }
        if (!createOrUpdateList.isEmpty()) {
            Models.data().createOrUpdateBatch(createOrUpdateList);
            List<ExcelWorkbookDefinition> needAddList = new ArrayList<>();
            MemoryIterableSearchCache<String, ExcelWorkbookDefinition> existWorkbookDefinitionCache = new MemoryIterableSearchCache<>(existWorkbookDefinitionList, this::keyGenerator);
            for (ExcelWorkbookDefinition item : createOrUpdateList) {
                if (existWorkbookDefinitionCache.get(keyGenerator(item)) == null) {
                    needAddList.add(item);
                }
            }
            existWorkbookDefinitionList.addAll(needAddList);
        }
        return existWorkbookDefinitionList;
    }

    private void refreshWorkbookDefinition(List<ExcelWorkbookDefinition> workbookDefinitionList) {
        try {
            excelFileService.refreshDefinitionContextBatch(workbookDefinitionList);
        } catch (Exception e) {
            log.error("Refresh excel workbook definition error.", e);
        }
    }

    private String keyGenerator(ExcelWorkbookDefinition item) {
        return item.getModel() + CharacterConstants.SEPARATOR_OCTOTHORPE + item.getName();
    }

    private ExcelWorkbookDefinition createOrUpdateExcelTemplate(TreeNodeXStream xs, View view) {
        String xmlTemplate = view.getTemplate();
        if (StringUtils.isBlank(xmlTemplate)) {
            return null;
        }
        String model = view.getModel();
        RequestContext requestContext = PamirsSession.getContext();
        if (requestContext == null) {
            return null;
        }
        ModelConfig modelConfig = requestContext.getModelConfig(model);
        if (modelConfig == null) {
            return null;
        }
        ModelTypeEnum modelType = modelConfig.getType();
        if (!ModelTypeEnum.STORE.equals(modelType) && !ModelTypeEnum.PROXY.equals(modelType)) {
            return null;
        }
        try {
            TreeNode<XMLNodeContent> viewRoot = xs.fromXML(xmlTemplate);
            XMLNodeContent rootContent = viewRoot.getValue();
            String type = rootContent.getAttribute(TYPE_KEY);
            if (StringUtils.isBlank(type)) {
                type = rootContent.getAttribute(WIDGET_KEY);
            }
            String viewType = type.trim().toUpperCase();
            if (StringUtils.isBlank(type) || (!ViewTypeEnum.TABLE.name().equals(viewType) && !ViewTypeEnum.FORM.name().equals(viewType))) {
                return null;
            }
            String templateDisplayName = generatorDefaultTemplateDisplayName(view, modelConfig, viewType);
            String sheetName = generatorDefaultSheetName(view, modelConfig, viewType);
            WorkbookDefinitionBuilder workbookDefinitionBuilder = WorkbookDefinitionBuilder.newInstance(model, FileConstant.DEFAULT_TEMPLATE_NAME + "_" + view.getName());
            workbookDefinitionBuilder.setBindingViewName(view.getName());
            BlockDefinitionBuilder blockDefinitionBuilder = workbookDefinitionBuilder.createSheet().setName(sheetName).createBlock(model, ExcelAnalysisTypeEnum.FIXED_HEADER, ExcelDirectionEnum.HORIZONTAL, 0, 1, 0, 12).setPresetNumber(10);
            HeaderDefinitionBuilder configHeaderDefinitionBuilder = blockDefinitionBuilder.createHeader().setStyleBuilder(ExcelHelper.createDefaultStyle()).setIsConfig(true);
            HeaderDefinitionBuilder headerDefinitionBuilder = blockDefinitionBuilder.createHeader().setStyleBuilder(ExcelHelper.createDefaultStyle(v -> v.setBold(true)));
            if (ViewTypeEnum.TABLE.name().equals(viewType)) {
                if (!createExportCell(configHeaderDefinitionBuilder, headerDefinitionBuilder, viewRoot, requestContext, model)) {
                    return null;
                }
                log.info("生成默认导出模板成功 [Model {}] [ViewName {}] [ViewType {}]", view.getModel(), view.getName(), view.getType());
                blockDefinitionBuilder.modifyDesignRange(0, 1, 0, configHeaderDefinitionBuilder.cellSize() - 1);
                return workbookDefinitionBuilder.setType(ExcelTemplateTypeEnum.EXPORT).setDisplayName(templateDisplayName).build().setTemplateSource(ExcelTemplateSourceEnum.SYSTEM);
            } else {
                if (!createImportCell(configHeaderDefinitionBuilder, headerDefinitionBuilder, viewRoot, requestContext, model)) {
                    return null;
                }
                log.info("生成默认导入模板成功 [Model {}] [ViewName {}] [ViewType {}]", view.getModel(), view.getName(), view.getType());
                blockDefinitionBuilder.modifyDesignRange(0, 1, 0, configHeaderDefinitionBuilder.cellSize() - 1);
                return workbookDefinitionBuilder.setType(ExcelTemplateTypeEnum.IMPORT).setDisplayName(templateDisplayName).build().setTemplateSource(ExcelTemplateSourceEnum.SYSTEM);
            }
        } catch (Exception e) {
            log.error("生成默认模板失败 [Model {}] [ViewName {}] [ViewType {}]", view.getModel(), view.getName(), view.getType(), e);
        }
        return null;
    }

    private String generatorDefaultTemplateDisplayName(View view, ModelConfig modelConfig, String viewType) {
        if (ViewTypeEnum.TABLE.name().equals(viewType.trim().toUpperCase())) {
            return generatorDefaultSheetName(view, modelConfig, viewType) + DEFAULT_EXPORT_TEMPLATE_SUFFIX;
        } else {
            return generatorDefaultSheetName(view, modelConfig, viewType) + DEFAULT_IMPORT_TEMPLATE_SUFFIX;
        }
    }

    private String generatorDefaultSheetName(View view, ModelConfig modelConfig, String viewType) {
        String name = view.getTitle();
        if (StringUtils.isBlank(name)) {
            name = modelConfig.getDisplayName();
            if (StringUtils.isBlank(name)) {
                if (ViewTypeEnum.TABLE.name().equals(viewType.trim().toUpperCase())) {
                    name = DEFAULT_EXPORT_TEMPLATE_DISPLAY_NAME;
                } else {
                    name = DEFAULT_IMPORT_TEMPLATE_DISPLAY_NAME;
                }
            }
        }
        return name;
    }

    private boolean createExportCell(HeaderDefinitionBuilder configHeaderDefinitionBuilder,
                                     HeaderDefinitionBuilder headerDefinitionBuilder,
                                     TreeNode<XMLNodeContent> viewRoot,
                                     RequestContext requestContext,
                                     String model) {
        boolean isCreate = false;
        for (TreeNode<XMLNodeContent> child : viewRoot.getChildren()) {
            XMLNodeContent childValue = child.getValue();
            String key = child.getKey();
            if (FIELD_KEY.equals(key)) {
                boolean invisible = childValue.getBooleanAttribute(FIELD_INVISIBLE_KEY);
                if (invisible) {
                    continue;
                }
                String field = childValue.getAttribute(DATA_KEY);
                if (StringUtils.isBlank(field)) {
                    continue;
                }
                String label = childValue.getAttribute(FIELD_LABEL_KEY);
                ModelFieldConfig modelFieldConfig = requestContext.getModelField(model, field);
                if (modelFieldConfig == null) {
                    continue;
                }
                if (Boolean.TRUE.equals(modelFieldConfig.getInvisible())) {
                    continue;
                }
                if (StringUtils.isBlank(label) || !BooleanHelper.isFalseWithoutException(label)) {
                    label = modelFieldConfig.getDisplayName();
                    if (StringUtils.isBlank(label)) {
                        label = field;
                    }
                }
                if (createCell(configHeaderDefinitionBuilder, headerDefinitionBuilder, requestContext, modelFieldConfig, field, label, true)) {
                    isCreate = true;
                }
            } else if (CONFIG_KEY.equals(key)) {
                String excludedActions = childValue.getAttribute(CONFIG_EXCLUDED_ACTIONS_KEY);
                if (excludedActions != null) {
                    if (excludedActions.contains(EXPORT_DIALOG_KEY)) {
                        return false;
                    }
                }
            } else if (TEMPLATE_KEY.equals(key)) {
                String slot = childValue.getAttribute(SLOT_KEY);
                if (StringUtils.isBlank(slot)) {
                    continue;
                }
                slot = slot.trim().toLowerCase();
                if (TABLE_KEY.equals(slot) || FIELDS_KEY.equals(slot)) {
                    isCreate = createExportCell(configHeaderDefinitionBuilder, headerDefinitionBuilder, child, requestContext, model);
                }
            }
        }
        return isCreate;
    }

    private boolean createImportCell(HeaderDefinitionBuilder configHeaderDefinitionBuilder,
                                     HeaderDefinitionBuilder headerDefinitionBuilder,
                                     TreeNode<XMLNodeContent> viewRoot,
                                     RequestContext requestContext,
                                     String model) {
        boolean isCreate = false;
        for (TreeNode<XMLNodeContent> child : viewRoot.getChildren()) {
            XMLNodeContent childValue = child.getValue();
            String key = child.getKey();
            if (FIELD_KEY.equals(key)) {
                boolean invisible = childValue.getBooleanAttribute(FIELD_INVISIBLE_KEY);
                if (invisible) {
                    continue;
                }
                String field = childValue.getAttribute(DATA_KEY);
                if (StringUtils.isBlank(field) || FieldConstants.CREATE_UID.equalsIgnoreCase(field) || FieldConstants.WRITE_UID.equalsIgnoreCase(field)) {
                    continue;
                }
                String label = childValue.getAttribute(FIELD_LABEL_KEY);
                ModelFieldConfig modelFieldConfig = requestContext.getModelField(model, field);
                if (modelFieldConfig == null) {
                    continue;
                }
                if (Boolean.TRUE.equals(modelFieldConfig.getInvisible())) {
                    continue;
                }
                if (StringUtils.isBlank(label) || !BooleanHelper.isFalseWithoutException(label)) {
                    label = modelFieldConfig.getDisplayName();
                    if (StringUtils.isBlank(label)) {
                        label = field;
                    }
                }
                if (createCell(configHeaderDefinitionBuilder, headerDefinitionBuilder, requestContext, modelFieldConfig, field, label, false)) {
                    isCreate = true;
                }
            } else if (CONFIG_KEY.equals(key)) {
                String excludedActions = childValue.getAttribute(CONFIG_EXCLUDED_ACTIONS_KEY);
                if (excludedActions != null) {
                    if (excludedActions.contains(EXPORT_DIALOG_KEY)) {
                        return false;
                    }
                }
            } else if (PACK_KEY.equals(key)) {
                isCreate = createImportCell(configHeaderDefinitionBuilder, headerDefinitionBuilder, child, requestContext, model);
            } else if (TEMPLATE_KEY.equals(key)) {
                String slot = childValue.getAttribute(SLOT_KEY);
                if (StringUtils.isBlank(slot)) {
                    continue;
                }
                slot = slot.trim().toLowerCase();
                if (TABLE_KEY.equals(slot) || FIELDS_KEY.equals(slot) || FORM_KEY.equals(slot)) {
                    isCreate = createImportCell(configHeaderDefinitionBuilder, headerDefinitionBuilder, child, requestContext, model);
                }
            }
        }
        return isCreate;
    }

    private boolean createCell(HeaderDefinitionBuilder configHeaderDefinitionBuilder,
                               HeaderDefinitionBuilder headerDefinitionBuilder,
                               RequestContext requestContext,
                               ModelFieldConfig modelFieldConfig,
                               String field,
                               String label,
                               Boolean isExport) {
        String ttype = modelFieldConfig.getTtype();
        if (TtypeEnum.RELATED.value().equals(ttype)) {
            if (Boolean.TRUE.equals(isExport) || Boolean.TRUE.equals(modelFieldConfig.getStore())) {
                ttype = modelFieldConfig.getRelatedTtype();
            } else {
                return false;
            }
        }
        boolean isMulti = Boolean.TRUE.equals(modelFieldConfig.getMulti());
        ExcelValueTypeEnum valueType = ExcelValueTypeEnum.STRING;
        String format = modelFieldConfig.getFormat();
        boolean isSampleField = true;
        if (TtypeEnum.STRING.value().equals(ttype) || TtypeEnum.INTEGER.value().equals(ttype)) {
            if (isMulti) {
                format = ExcelHelper.generatorMultiValueFormatExpression();
                valueType = ExcelValueTypeEnum.OBJECT;
            }
            else if (TtypeEnum.INTEGER.value().equals(ttype)) {
                format = ExcelValueTypeEnum.STRING.getDefaultFormat();
                valueType = ExcelValueTypeEnum.STRING;
            }
        } else if (TtypeEnum.ENUM.value().equals(ttype)) {
            Map<String, String> enumerationMapping = new HashMap<>();
            DataDictionary dictionary = requestContext.getDictionary(modelFieldConfig.getDictionary());
            for (DataDictionaryItem dictionaryItem : dictionary.getOptions()) {
                enumerationMapping.put(dictionaryItem.getValue(), dictionaryItem.getDisplayName());
            }
            format = JSON.toJSONString(enumerationMapping);
            valueType = ExcelValueTypeEnum.ENUMERATION;
        } else if (TtypeEnum.DATE.value().equals(ttype)) {
            if (StringUtils.isBlank(format)) {
                format = DateFormatEnum.DATE.value();
            }
            valueType = ExcelValueTypeEnum.DATETIME;
        } else if (TtypeEnum.TIME.value().equals(ttype)) {
            if (StringUtils.isBlank(format)) {
                format = DateFormatEnum.TIME.value();
            }
            valueType = ExcelValueTypeEnum.DATETIME;
        } else if (TtypeEnum.DATETIME.value().equals(ttype)) {
            if (StringUtils.isBlank(format)) {
                format = DateFormatEnum.DATETIME.value();
            }
            valueType = ExcelValueTypeEnum.DATETIME;
        } else if (TtypeEnum.BOOLEAN.value().equals(ttype)) {
            format = ExcelValueTypeEnum.BOOLEAN.getDefaultFormat();
            valueType = ExcelValueTypeEnum.BOOLEAN;
        } else if (TtypeEnum.MONEY.value().equals(ttype) || TtypeEnum.FLOAT.value().equals(ttype)) {
            if (isMulti) {
                format = ExcelHelper.generatorMultiValueFormatExpression();
                valueType = ExcelValueTypeEnum.OBJECT;
            } else {
                format = ExcelHelper.getNumberFormat(modelFieldConfig.getDecimal());
                valueType = ExcelValueTypeEnum.NUMBER;
            }
        } else if (TtypeEnum.M2O.value().equals(ttype) || TtypeEnum.O2O.value().equals(ttype)) {
            relationFieldProcess(configHeaderDefinitionBuilder, headerDefinitionBuilder, requestContext, modelFieldConfig, field, label, false, isExport);
            isSampleField = false;
        } else if (TtypeEnum.O2M.value().equals(ttype) || TtypeEnum.M2M.value().equals(ttype)) {
            relationFieldProcess(configHeaderDefinitionBuilder, headerDefinitionBuilder, requestContext, modelFieldConfig, field, label, true, isExport);
            isSampleField = false;
        }
        if (isSampleField) {
            configHeaderDefinitionBuilder.createCell().setField(field).setType(valueType).setFormat(format);
            headerDefinitionBuilder.createCell().setValue(label);
        }
        return true;
    }

    private void relationFieldProcess(HeaderDefinitionBuilder configHeaderDefinitionBuilder,
                                      HeaderDefinitionBuilder headerDefinitionBuilder,
                                      RequestContext requestContext,
                                      ModelFieldConfig modelFieldConfig,
                                      String field,
                                      String label,
                                      Boolean isMulti,
                                      Boolean isExport) {
        if (isExport) {
            exportRelationFieldProcess(configHeaderDefinitionBuilder, headerDefinitionBuilder, requestContext, modelFieldConfig, field, label, isMulti);
        } else {
            importRelationFieldProcess(configHeaderDefinitionBuilder, headerDefinitionBuilder, requestContext, modelFieldConfig, field, label, isMulti);
        }
    }

    private void exportRelationFieldProcess(HeaderDefinitionBuilder configHeaderDefinitionBuilder,
                                            HeaderDefinitionBuilder headerDefinitionBuilder,
                                            RequestContext requestContext,
                                            ModelFieldConfig modelFieldConfig,
                                            String field,
                                            String label,
                                            Boolean isMulti) {
        String referenceModel = modelFieldConfig.getReferences();
        ModelConfig referenceModelConfig = getReferenceModelConfig(requestContext, modelFieldConfig);
        if (referenceModelConfig == null) {
            return;
        }
        List<String> optionLabels = Optional.ofNullable(referenceModelConfig.getModelDefinition()).map(ModelDefinition::getLabelFields).filter(CollectionUtils::isNotEmpty).orElse(null);
        if (CollectionUtils.isEmpty(optionLabels)) {
            ModelFieldConfig labelFieldConfig = requestContext.getModelField(referenceModel, FieldConstants.NAME);
            if (labelFieldConfig == null) {
                labelFieldConfig = requestContext.getModelField(referenceModel, FieldConstants.CODE);
            }
            if (labelFieldConfig == null) {
                labelFieldConfig = requestContext.getModelField(referenceModel, FieldConstants.ID);
            }
            if (labelFieldConfig == null) {
                return;
            }
            optionLabels = Collections.singletonList(labelFieldConfig.getField());
        }
        String optionLabel = optionLabels.get(0);
        String formatExpression;
        if (isMulti) {
            formatExpression = ExcelHelper.generatorMultiObjectFormatExpression(referenceModel, optionLabel);
        } else {
            formatExpression = ExcelHelper.generatorSingleObjectFormatExpression(optionLabel);
        }
        configHeaderDefinitionBuilder.createCell().setField(field).setType(ExcelValueTypeEnum.OBJECT).setFormat(formatExpression);
        headerDefinitionBuilder.createCell().setValue(label);
    }

    /**
     * 导入关联关系处理仅处理包含唯一键或主键的多对一字段
     */
    private void importRelationFieldProcess(HeaderDefinitionBuilder configHeaderDefinitionBuilder,
                                            HeaderDefinitionBuilder headerDefinitionBuilder,
                                            RequestContext requestContext,
                                            ModelFieldConfig modelFieldConfig,
                                            String field,
                                            String label,
                                            Boolean isMulti) {
        if (isMulti) {
            // 过滤所有 o2m/m2m 类型字段
            return;
        }
        String referenceModel = modelFieldConfig.getReferences();
        ModelConfig referenceModelConfig = getReferenceModelConfig(requestContext, modelFieldConfig);
        if (referenceModelConfig == null) {
            return;
        }
        //此处仅处理指定唯一索引的关联对象
        List<String> uniqueList = referenceModelConfig.getUniques();
        if (CollectionUtils.isEmpty(uniqueList)) {
            return;
        }
        List<String> labelFields = Optional.ofNullable(referenceModelConfig.getModelDefinition()).map(ModelDefinition::getLabelFields).filter(CollectionUtils::isNotEmpty).orElse(null);
        if (CollectionUtils.isEmpty(labelFields)) {
            return;
        }
        String firstUnique = uniqueList.get(0);
        String[] uniques = firstUnique.split(CharacterConstants.SEPARATOR_COMMA);
        Map<String, String> referenceFieldMap = new LinkedHashMap<>();
        Map<String, ModelFieldConfig> referenceFieldConfigMap = new LinkedHashMap<>();
        List<String> relationFields = new ArrayList<>(Arrays.asList(uniques));
        relationFields.add(labelFields.get(0));
        for (String relationField : relationFields) {
            relationField = relationField.trim();
            ModelFieldConfig referenceModelFieldConfig = requestContext.getModelField(referenceModel, relationField);
            if (referenceModelFieldConfig == null) {
                return;
            }
            String referenceTtype = referenceModelFieldConfig.getTtype();
            if (TtypeEnum.isRelationType(referenceTtype)) {
                return;
            }
            String referenceLabel = referenceModelFieldConfig.getDisplayName();
            if (StringUtils.isBlank(referenceLabel)) {
                referenceLabel = relationField;
            }
            String newRelationField;
            if (isMulti) {
                newRelationField = field + FileConstant.LIST_FLAG_CHARACTER + FileConstant.POINT_CHARACTER + relationField;
            } else {
                newRelationField = field + FileConstant.POINT_CHARACTER + relationField;
            }
            referenceFieldMap.put(newRelationField, label + DEFAULT_RELATION_LABEL_SPLIT + referenceLabel);
            referenceFieldConfigMap.put(newRelationField, referenceModelFieldConfig);
        }
        for (Map.Entry<String, String> entry : referenceFieldMap.entrySet()) {
            String key = entry.getKey();
            createCell(configHeaderDefinitionBuilder, headerDefinitionBuilder, requestContext, referenceFieldConfigMap.get(key), key, entry.getValue(), null);
        }
    }

    private ModelConfig getReferenceModelConfig(RequestContext requestContext, ModelFieldConfig modelFieldConfig) {
        String referenceModel = modelFieldConfig.getReferences();
        ModelConfig referenceModelConfig = requestContext.getModelConfig(referenceModel);
        if (referenceModelConfig == null) {
            log.error("Invalid reference model config. model: {}, field: {}, referenceModel: {}",
                    modelFieldConfig.getModel(), modelFieldConfig.getField(), referenceModel);
            return null;
        }
        return referenceModelConfig;
    }
}

效果图

导入导出支持国际化语言分隔符

Oinone社区 作者:yexiu原创文章,如若转载,请注明出处:https://doc.oinone.top/other/25118.html

访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验

(0)
yexiu的头像yexiu数式员工
上一篇 1天前
下一篇 2024年2月28日 pm10:15

相关推荐

  • 打印支持非存储数据导出

    介绍 平台提供的默认打印功能没有支持非存储数据的导出。我们可以自定义打印导出功能,以满足业务中个性化的需求。 实现思路 重写打印任务模型,添加业务数据字段 自定义打印动作,前端将导出数据放到业务数据字段中 使用导出数据扩展点机制修改导出数据 代码示例 继承平台的打印任务模型,加上需要业务数据字段,这个字段用于传输需要打印的表单数据,但是需要自定义打印的表单往往不止一个,所以需要定义为通用的Object字段。 @Model.model(TransientPdfPrintTask.MODEL_MODEL) @Model(displayName = "传输模型Pdf打印任务") public class TransientPdfPrintTask extends PdfPrintTask { public static final String MODEL_MODEL="demo.TransientPdfPrintTask"; @Field(displayName = "业务数据") private Object businessData; } 自定义打印动作 @Model.model(TransientPdfPrintTask.MODEL_MODEL) @Component public class TransientPdfPrintTaskAction extends AbstractPdfPrintTaskAction<TransientPdfPrintTask> { @Resource private PdfFileService pdfFileService; @Action(displayName = "打印", contextType = ActionContextTypeEnum.CONTEXT_FREE, bindingType = {ViewTypeEnum.TABLE}) @Override public TransientPdfPrintTask createPrintTask(TransientPdfPrintTask data) { return super.createPrintTask(data); } @Override protected void doExport(TransientPdfPrintTask exportTask, PdfDefinitionContext context) { pdfFileService.doExportSync(exportTask, context); } @Function.Advanced(type = FunctionTypeEnum.QUERY) @Function(openLevel = FunctionOpenEnum.API) public TransientPdfPrintTask construct(TransientPdfPrintTask data) { String model = FetchUtil.fetchVariables(PdfConstants.MODEL); data.construct(); data.setModel(model); return data; } } 本篇文章只介绍同步打印,如果异步需要修改doExport方法。 编写导出的数据处理逻辑 @Ext(PdfPrintTask.class) public class PrintExportExt extends AbstractPdfExportFetchDataExtPointImpl implements PdfExportFetchDataExtPoint { // 这里使用扩展点表达式匹配需要打印的非存储模型,只有表达式为true才会走这段逻辑。 @Override @ExtPoint.Implement(expression = "context.model==\"" + ProductPricingClientTransient.MODEL_MODEL + "\"") public List<Object> fetchExportData(PdfPrintTask exportTask, PdfDefinitionContext context) { List<Object> result = new ArrayList<>(); List<Object> dataList = new ArrayList<>(); TransientPdfPrintTask transientPdfPrintTask = new TransientPdfPrintTask(); transientPdfPrintTask.set_d(exportTask.get_d()); dataList.add(transientPdfPrintTask.getBusinessData()); result.add(dataList); return result; } } 前端自定义打印按钮,将数据提交给业务数据字段,并使用同步导出打印。

    2025年10月13日
    37000
  • 后端脚手架生成工程

    后端脚手架生成工程 1、使用如下命令来利用项目脚手架生成工程:新建archetype-project-generate.sh(bat) 脚本 archetype-project-generate.sh #!/bin/bash # 项目生成脚手架 # 用于新项目的构建 # 脚手架使用目录 # 本地 local # 本地脚手架信息存储路径 ~/.m2/repository/archetype-catalog.xml archetypeCatalog=local # 以下参数以pamirs-demo为例 # 新项目的groupId groupId=pro.shushi.pamirs.demo # 新项目的artifactId artifactId=pamirs-demo # 新项目的version version=1.0.0-SNAPSHOT # Java包名前缀 packagePrefix=pro.shushi # Java包名后缀 packageSuffix=pamirs.demo # 新项目的pamirs platform version pamirsVersion=6.2.8 # Java类名称前缀 javaClassNamePrefix=Demo # 项目名称 module.displayName projectName=OinoneDemo # 模块 MODULE_MODULE 常量 moduleModule=demo_core # 模块 MODULE_NAME 常量 moduleName=DemoCore # spring.application.name applicationName=pamirs-demo # tomcat server address serverAddress=0.0.0.0 # tomcat server port serverPort=8090 # redis host redisHost=127.0.0.1 # redis port redisPort=6379 # 数据库名 db=demo # zookeeper connect string zkConnectString=127.0.0.1:2181 # zookeeper rootPath zkRootPath=/demo mvn archetype:generate \ -DinteractiveMode=false \ -DarchetypeCatalog=${archetypeCatalog} \ -DarchetypeGroupId=pro.shushi.pamirs.archetype \ -DarchetypeArtifactId=pamirs-project-archetype \ -DarchetypeVersion=6.2.8 \ -DgroupId=${groupId} \ -DartifactId=${artifactId} \ -Dversion=${version} \ -DpamirsVersion=${pamirsVersion} \ -Dpackage=${packagePrefix}.${packageSuffix} \ -DpackagePrefix=${packagePrefix} \ -DpackageSuffix=${packageSuffix} \ -DjavaClassNamePrefix=${javaClassNamePrefix} \ -DprojectName="${projectName}" \ -DmoduleModule=${moduleModule} \ -DmoduleName=${moduleName} \ -DapplicationName=${applicationName} \ -DserverAddress=${serverAddress} \ -DserverPort=${serverPort} \ -DredisHost=${redisHost} \ -DredisPort=${redisPort} \ -Ddb=${db} \ -DzkConnectString=${zkConnectString} \ -DzkRootPath=${zkRootPath} archetype-project-generate.bat @echo off :: 项目生成脚手架 set archetypeCatalog=local set groupId=pro.shushi.pamirs.demo set artifactId=pamirs-demo set version=1.0.0-SNAPSHOT set packagePrefix=pro.shushi set packageSuffix=pamirs.demo set pamirsVersion=6.2.8 set javaClassNamePrefix=Demo set projectName=OinoneDemo set moduleModule=demo_core set moduleName=DemoCore set applicationName=pamirs-demo set serverAddress=0.0.0.0 set serverPort=8090 set redisHost=127.0.0.1 set redisPort=6379 set db=demo set…

    2025年8月22日
    43300
  • 如何通过业务数据拿到工作流用户任务待办

    在模型里面建一个非存储字段,用来传输工作流用户任务待办ID。。 界面设计器把这个字段拖到列表页里,并在跳转动作上配置上下文参数,把任务待办id传到表单页里。 重写教师模型的queryPage,通过业务数据id查询出每条业务数据的工作流用户任务待办id返回给前端。 @Function.Advanced(displayName = "查询教师列表", type = FunctionTypeEnum.QUERY, category = FunctionCategoryEnum.QUERY_PAGE, managed = true) @Function(openLevel = {FunctionOpenEnum.LOCAL, FunctionOpenEnum.REMOTE, FunctionOpenEnum.API}) public Pagination<Teacher> queryPage(Pagination<Teacher> page, IWrapper<Teacher> queryWrapper) { Pagination<Teacher> teacherPagination = new Teacher().queryPage(page, queryWrapper); List<Teacher> content = teacherPagination.getContent(); if (CollectionUtils.isEmpty(content)) { return teacherPagination; } List<Long> teacherIds = content.stream().map(Teacher::getId).collect(Collectors.toList()); List<WorkflowUserTask> workflowUserTasks = Models.data().queryListByWrapper(Pops.<WorkflowUserTask>lambdaQuery() .from(WorkflowUserTask.MODEL_MODEL) .in(WorkflowUserTask::getNodeDataBizId, teacherIds) .orderByDesc(WorkflowUserTask::getCreateDate) ); if (CollectionUtils.isEmpty(workflowUserTasks)) { return teacherPagination; } Map<Long/*业务id*/, WorkflowUserTask> userTaskMap = workflowUserTasks.stream().collect(Collectors.toMap( WorkflowUserTask::getNodeDataBizId, a -> a, (old, n) -> old) ); for (Teacher teacher : content) { if (userTaskMap.containsKey(teacher.getId())) { teacher.setWorkflowUserTaskId(userTaskMap.get(teacher.getId()).getId()); } } return teacherPagination; } 查看效果,任务待办id成功传到表单里面。

    2025年1月10日
    1.7K00
  • 设计器基础学习路径

    模块 内容 目标 doc 链接 模型设计器 模型 1.熟悉模型管理和字段管理 模型 数据字典 熟悉数据字典的创建 数据字典 数据编码 了解数据编码的操作创建 数据编码 界面设计器 了解页面 了解界面设计器中的页面 页面 页面设计 增删改查 【界面设计器】模型增删改查基础 页面设计 左树右表 【界面设计器】左树右表 页面设计 树形表格 【界面设计器】树形表格 页面设计 树下拉 【界面设计器】树下拉/级联 页面设计 自定义组件基础 【界面设计器】自定义字段组件基础 页面设计 熟悉页面设计的操作 页面设计 自定义组件 熟悉如何使用自定义组件 自定义组件 流程设计器 流程组成 了解流程的组成 流程 流程设计 熟悉流程设计内容 流程设计 熟悉流程的触发节点 流程触发 熟悉流程的节点动作与设计使用 节点动作 低代码与无代码结合 示例讲解 Outsourcing相关 低无一体的开发方式、设计数据的导入导出等

    2024年6月15日
    80700

Leave a Reply

登录后才能评论