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

需求

  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 {

    @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) {
        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 model = 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()) {
            ModelFieldConfig modelField = PamirsSession.getContext().getModelField(model, key);
            if (modelField == null) {
                continue;
            }
            String ttype = modelField.getTtype();
            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;
    }

    /**
     * 小数格式化
     */
    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);
    }
}
  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低代码应用平台体验

Like (0)
yexiu's avataryexiu数式员工
Previous 2025年11月17日 am9:57
Next 2025年12月17日 pm12:00

相关推荐

  • 外部系统接入SSO-OAuth2(6.3.x 之后版本支持)

    一、概述 统一身份认证系统提供了单点登录功能。本文档详述了统一身份认证系统单点登录接口,介绍了应用系统对接统一身份认证系统的过程和方法,用以帮助开发人员将应用系统接入统一身份认证系统。 本文档支持的协议:OAuth2 二、统一认证集成规范 2.1 SSO登录场景 序号 场景 预期结果 确认 1 登录跳转,在未登录的条件下,直接访问业务系统 跳转到单点登录界面 是 2 登录跳转,在已登录的条件下,直接访问业务系统 直接进入业务系统 是 3 正常登录,先登录认证,然后访问业务系统 直接进入业务系统 是 4 正常登出,在单点登录系统登出 访问业务系统要重新登录 是 5 正常登出,在业务系统登出 单点登录系统同步登出 是 2.2 认证流程 OAuth2 登录和认证流程 后端验证 token 后,创建本地会话(如 JSESSIONID 或自定义 Cookie); 后续应用请求靠本地会话维持登录状态,不再依赖原始 token; 本地会话有过期时间(如 30 分钟无操作过期); 三、认证服务接口 3.1 SSO服务端认证 浏览器登录 分类 说明 请求地址(开发环境) http://${host}:${port}/pamirs/sso/auth?client_id={client_id}&redirect_uri={redirect_uri}&state={state} 请求地址(测试环境) http://${host}:${port}/pamirs/sso/auth?client_id={client_id}&redirect_uri={redirect_uri}&state={state} 请求方式 GET 请求参数 redirect_uri:应用系统回调地址client_id:SSO 服务端颁发的应用 IDstate:可选但推荐,用于防止 CSRF 的随机字符串(UUID 随机码) 请求示例 http://${host}:${port}/pamirs/sso/auth?client_id=client123456&redirect_rui=http://app.example.com&state=abc123 响应说明 1. 已登录:重定向到 redirect 地址并携带授权码 code,如 http://app.example.com&state=abc123&code=xxx2. 未登录:重定向到服务器 SSO 登录地址 备注 SSO 登录成功后,会携带授权码并重定向到 redirect 地址 3.2 验证授权码 分类 说明 请求地址(开发环境) http://${host}:${port}/pamirs/sso/oauth2/authorize 请求地址(测试环境) http://${host}:${port}/pamirs/sso/oauth2/authorize 请求方式 POST 请求 Body {"grant_type":"authorization_code","client_id":"xxxx","client_secret":"xxxxx","code":"xxxx"} 请求示例 http://${host}:${port}/pamirs/sso/oauth2/authorize 响应说明 {"code":"0","msg":"成功","data":{"access_token":"xxxxx","expires_in":7200,"refresh_token":"xxxxxx","refresh_token_expiresIn":604800}} 备注 — 3.2 根据token获取用户信息 分类 说明 请求地址(开发环境) http://${host}:${port}/pamirs/sso/oauth2/getUserInfo 请求地址(测试环境) http://${host}:${port}/pamirs/sso/oauth2/getUserInfo 请求方式 POST 请求头 Authorization: Bearer xxxxxxxx 请求 Body {"client_id":"xxxx"} 请求示例 http://${host}:${port}/pamirs/sso/oauth2/getUserInfo 响应 {"code":"0","msg":"成功","data":{"name":"xxxxx","email":"xxxxx","login":"xxxxxx","id":"xxxxxxx"}} 备注 成功返回用户信息;失败返回错误码:1 根据实际情况其他协议的验证接口 3.3 单点登出接口 分类 说明 请求地址(开发环境) http://${host}:${port}/pamirs/sso/oauth2/logout 请求地址(测试环境) http://${host}:${port}/pamirs/sso/oauth2/logout 请求方式 POST 请求头 Authorization: Bearer xxxxxxxx 请求 Body {"client_id":"xxxx"} 请求示例 http://${host}:${port}/pamirs/sso/oauth2/logout 响应头 HTTP/1.1 200;Content-Type: application/html;charset=UTF-8 响应体 登出成功页面 备注 应用系统向 SSO 系统发起登出请求,SSO 收到后会通知所有其它系统登出该用户 四、服务注册 添加以下依赖 <!– sso相关 –> <dependency> <groupId>pro.shushi.pamirs.core</groupId> <artifactId>pamirs-sso-api</artifactId> </dependency> <dependency> <groupId>pro.shushi.pamirs.core</groupId> <artifactId>pamirs-sso-common</artifactId> </dependency> <dependency> <groupId>pro.shushi.pamirs.core</groupId> <artifactId>pamirs-sso-server</artifactId> </dependency> 配置文件 pamirs sso: enabled: true server: loginUrl: http://127.0.0.1:8091/login authType:…

    2026年1月7日
    61400
  • 前端发布接入jenkins

    最原始的前端发布,会经过本地打包、压个 zip 包、通过工具手动上传、找到 leader 帮忙解压到对应的服务器上、同步文件服务器等等的步骤。每一个环节都是人工操作,发个版非常的繁琐。接入jenkins有助于我们简化CI/CD流程,实现前端发布自动化。 1. jenkins 安装部署(docker) 1-1 前置条件 安装 git、docker、配置 ssh git 安装 # enter 到底 yum install -y git # 查看git版本号 验证git安装成功 # git version 1.8.3.1 git –version docker 安装 # docker-ce Docker社区版 # docker-ce-cli Docker命令行界面(CLI) # containerd.io Docker插件,直接调用 Docker Compose # docker-compose-plugin Docker插件,直接调用 Docker Compose yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin 配置 ssh # Enter到底,最终会生成以下文件 # /root/.ssh/authorized_keys 允许无密码登录的公钥列表 # /root/.ssh/id_rsa 私钥文件 # /root/.ssh/id_rsa.pub 公钥文件 注意该文件里的内容是接下来要用的 ssh-keygen -t rsa -C "root" # 复制公钥文件的内容,添加到GitHub 的 SSH keys 或 任意其他远程仓库 vim /root/.ssh/id_rsa.pub 1-2 jenkins 安装 docker 拉取镜像 # 拉取nginx docker pull nginx # 拉取jenkins docker pull jenkins/jenkins:lts # 查看镜像是否安装成功 docker images # REPOSITORY TAG IMAGE ID CREATED SIZE # jenkins/jenkins lts 6a44d1dd2d60 3 weeks ago 468MB # nginx latest 53a18edff809 7 weeks ago 192MB 创建 docker 相关目录 # 创建docker的相关目录 mkdir -p ./docker/{compose,jenkins_home,nginx/conf,html/origin/{master,dev}} # 创建docker-compose.yml配置文件 cd ./docker/compose # 具体配置内容见下面 touch docker-compose.yml # 创建nginx.conf配置文件 cd ./docker/nginx/conf # 具体配置内容见下面 touch nginx.conf 最终目录结构如下 ./docker/ ├── compose/ │ └── docker-compose.yml # 空的 docker-compose 配置文件 └── html/ └── origin/ ├── master/ # 预留的 master 版本 HTML 目录(为空) └── dev/ # 预留的 dev 版本 HTML…

    2025年5月12日
    69300
  • Oinone 初级学习路径

    文档说明 文档链接 介绍Oinone前端相关知识点 前端基础学习路径 介绍Oinone后端相关知识点 后端基础学习路径 介绍平台基础组件 平台基础组件 介绍平台设计器常用场景实操 设计器基础学习路径 设计器实操案例示例 7.2 实战训练(积分发放)

    2024年6月15日
    1.5K00
  • 设计器基础学习路径

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

    2024年6月15日
    1.5K00
  • 前端学习路径

    准备工作 环境准备-1h安装基础环境,需要Node和PNPM如果已经安装检查对应版本,体验阶段建议版本保持一致 克隆一个空工程-0.5h全新工程 注意事项-0.5h静态资源解压,如果还没有对应的后端服务。可以使用:https://demo.oinone.top(Oinone演示环境),账号密码:admin/admin 前端基础 1.1.1了解组件-3h– 组件如何开发– 组件如何复用– 组件如何嵌套 1.1.2 更近一步-5h– 构建更复杂的工作台– 如何发起后端请求– 构建一个通用的图表组件 1.1.3 深入了解概念-5h– 通用概念、名词解释– 页面渲染原理– 组件执行原理 1.1.4业务实战-实现通用的甘特图组件-8h– 怎么与第三方结合– 如何在开源组件上接入Oinone, 并复用 1.1.5 业务实战-实现通用的画廊组件-6h–如何更近一步复用–基础学习结束 前端进阶 2.1.1代码和无代码如何结合-4h– 了解无代码如何使用和基础概念 2.1.2使用无代码搭建增删改查-8h– 了解无代码如何使用和基础概念 最后一步 当您实现玩基础和进阶的所有的效果,Oinone的整体使用您已经大致掌握了,接下来您可以找一个当前业务的场景来实现它把!

    2025年9月1日
    1.1K00

Leave a Reply

Please Login to Comment