zhou zhou
9 小时以前 e12fb4e6e8e0a408e81ce05a269a15cc535d8c78
#生成器
8个文件已添加
1个文件已修改
1978 ■■■■■ 已修改文件
rsf-framework/src/main/java/com/vincent/rsf/framework/generators/RsfDesignGenerator.java 946 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-framework/src/main/resources/templates/rsf-design/Api.txt 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-framework/src/main/resources/templates/rsf-design/Controller.txt 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-framework/src/main/resources/templates/rsf-design/EditDialog.txt 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-framework/src/main/resources/templates/rsf-design/Index.txt 240 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-framework/src/main/resources/templates/rsf-design/PageHelpers.txt 245 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-framework/src/main/resources/templates/rsf-design/Search.txt 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-framework/src/main/resources/templates/rsf-design/TableColumns.txt 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/CodeBuilder.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-framework/src/main/java/com/vincent/rsf/framework/generators/RsfDesignGenerator.java
New file
@@ -0,0 +1,946 @@
package com.vincent.rsf.framework.generators;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.generators.constant.SqlOsType;
import com.vincent.rsf.framework.generators.domain.Column;
import com.vincent.rsf.framework.generators.utils.GeneratorUtils;
import org.springframework.core.io.ClassPathResource;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class RsfDesignGenerator {
    private static final String BASE_DIR = "src/main/";
    private static final String JAVA_DIR = BASE_DIR + "java/";
    private static final String MODULES_DIR = "modules/";
    private static final List<String> MANAGED_FIELDS = Arrays.asList(
            "deleted",
            "tenantId",
            "createBy",
            "createTime",
            "updateBy",
            "updateTime",
            "version"
    );
    private static final List<String> SEARCH_EXCLUDED_FIELDS = Arrays.asList(
            "deleted",
            "tenantId",
            "createBy",
            "createTime",
            "updateBy",
            "updateTime",
            "version"
    );
    public String url;
    public String username;
    public String password;
    public String table;
    public String tableDesc;
    public String packagePath;
    public boolean controller = true;
    public boolean service = true;
    public boolean mapper = true;
    public boolean entity = true;
    public boolean xml = true;
    public boolean frontend = true;
    public boolean sql = true;
    public SqlOsType sqlOsType;
    public String backendPrefixPath;
    public String frontendPrefixPath;
    public String frontendViewPath;
    public String frontendApiModule;
    private List<Column> columns = new ArrayList<>();
    private String fullEntityName;
    private String simpleEntityName;
    private String kebabEntityName;
    private String constantPrefix;
    private String primaryKeyColumn;
    private String majorColumn;
    private String itemName;
    private String normalizedFrontendViewPath;
    private String normalizedFrontendApiModule;
    public void build() throws Exception {
        init();
        buildBackendArtifacts();
        if (controller) {
            writeTemplate("Controller", resolveControllerDirectory(), fullEntityName + "Controller.java");
        }
        if (frontend) {
            buildFrontendArtifacts();
        }
    }
    private void init() throws Exception {
        validateBaseConfig();
        gainDbInfo();
        fullEntityName = GeneratorUtils.getNameSpace(table);
        simpleEntityName = GeneratorUtils.firstCharConvert(fullEntityName, true);
        kebabEntityName = humpToKebab(simpleEntityName);
        constantPrefix = GeneratorUtils.humpToLine(simpleEntityName).toUpperCase();
        primaryKeyColumn = resolvePrimaryKeyColumn();
        majorColumn = resolveMajorColumn();
        String[] packagePathSplit = packagePath.split("\\.");
        itemName = packagePathSplit[packagePathSplit.length - 1];
        normalizedFrontendViewPath = normalizePath(frontendViewPath);
        normalizedFrontendApiModule = normalizeApiModule(frontendApiModule);
    }
    private void validateBaseConfig() {
        if (this.sqlOsType == null) {
            throw new RuntimeException("请选择sqlOsType!");
        }
        if (Cools.isEmpty(this.table)) {
            throw new RuntimeException("请输入table!");
        }
        if (Cools.isEmpty(this.tableDesc)) {
            throw new RuntimeException("请输入tableDesc!");
        }
        if (Cools.isEmpty(this.packagePath)) {
            throw new RuntimeException("请输入packagePath!");
        }
        if (frontend) {
            if (Cools.isEmpty(frontendPrefixPath)) {
                throw new RuntimeException("请输入frontendPrefixPath!");
            }
            if (Cools.isEmpty(frontendViewPath)) {
                throw new RuntimeException("请输入frontendViewPath!");
            }
            if (Cools.isEmpty(frontendApiModule)) {
                throw new RuntimeException("请输入frontendApiModule!");
            }
        }
    }
    private void gainDbInfo() throws Exception {
        Connection connection = null;
        try {
            switch (this.sqlOsType) {
                case MYSQL:
                    Class.forName("com.mysql.cj.jdbc.Driver").newInstance();
                    connection = DriverManager.getConnection("jdbc:mysql://" + url, username, password);
                    this.columns = ReactGenerator.getMysqlColumns(connection, table, true, sqlOsType);
                    break;
                case SQL_SERVER:
                    Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver").newInstance();
                    connection = DriverManager.getConnection("jdbc:sqlserver://" + url, username, password);
                    this.columns = ReactGenerator.getSqlServerColumns(connection, table, true, sqlOsType);
                    break;
                default:
                    throw new RuntimeException("请指定数据库类型!");
            }
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (Exception ignore) {
                }
            }
        }
    }
    private void buildBackendArtifacts() throws Exception {
        ReactGenerator generator = new ReactGenerator();
        generator.url = url;
        generator.username = username;
        generator.password = password;
        generator.table = table;
        generator.tableDesc = tableDesc;
        generator.packagePath = packagePath;
        generator.controller = false;
        generator.service = service;
        generator.mapper = mapper;
        generator.entity = entity;
        generator.xml = xml;
        generator.react = false;
        generator.sql = sql;
        generator.sqlOsType = sqlOsType;
        generator.backendPrefixPath = backendPrefixPath;
        generator.frontendPrefixPath = frontendPrefixPath;
        generator.build();
    }
    private void buildFrontendArtifacts() throws IOException {
        String pageDirectory = resolveFrontendPageDirectory();
        String modulesDirectory = pageDirectory + MODULES_DIR;
        writeTemplate("Index", pageDirectory, "index.vue");
        writeTemplate("PageHelpers", pageDirectory, simpleEntityName + "Page.helpers.js");
        writeTemplate("TableColumns", pageDirectory, simpleEntityName + "Table.columns.js");
        writeTemplate("Search", modulesDirectory, kebabEntityName + "-search.vue");
        writeTemplate("EditDialog", modulesDirectory, kebabEntityName + "-edit-dialog.vue");
        writeTemplate("Api", resolveFrontendApiDirectory(), normalizedFrontendApiModule + ".js");
    }
    private String resolveControllerDirectory() {
        return ensureTrailingSlash(backendPrefixPath)
                + JAVA_DIR
                + packagePath.replace(".", "/")
                + "/controller/";
    }
    private String resolveFrontendPageDirectory() {
        return ensureTrailingSlash(frontendPrefixPath) + "src/views/" + normalizedFrontendViewPath + "/";
    }
    private String resolveFrontendApiDirectory() {
        String directory = ensureTrailingSlash(frontendPrefixPath) + "src/api/";
        int index = normalizedFrontendApiModule.lastIndexOf('/');
        if (index < 0) {
            return directory;
        }
        return directory + normalizedFrontendApiModule.substring(0, index + 1);
    }
    private void writeTemplate(String templateName, String directory, String fileName) throws IOException {
        String content = readTemplate(templateName);
        writeFile(applyReplacements(content), directory, fileName, templateName);
    }
    private String readTemplate(String templateName) throws IOException {
        StringBuilder builder = new StringBuilder();
        ClassPathResource classPath = new ClassPathResource("templates/rsf-design/" + templateName + ".txt");
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(classPath.getInputStream(), StandardCharsets.UTF_8))) {
            String lineContent;
            while ((lineContent = reader.readLine()) != null) {
                builder.append(lineContent).append("\n");
            }
        }
        return builder.toString();
    }
    private String applyReplacements(String content) {
        Map<String, String> replacements = buildReplacements();
        String resolved = content;
        for (Map.Entry<String, String> entry : replacements.entrySet()) {
            resolved = resolved.replace("@{" + entry.getKey() + "}", Objects.toString(entry.getValue(), ""));
        }
        return resolved;
    }
    private Map<String, String> buildReplacements() {
        Map<String, String> replacements = new LinkedHashMap<>();
        replacements.put("TABLENAME", table);
        replacements.put("TABLEDESC", safeText(tableDesc));
        replacements.put("ENTITYNAME", fullEntityName);
        replacements.put("SIMPLEENTITYNAME", simpleEntityName);
        replacements.put("UENTITYNAME", GeneratorUtils.firstCharConvert(simpleEntityName, false));
        replacements.put("KEBABENTITYNAME", kebabEntityName);
        replacements.put("COMPANYNAME", packagePath);
        replacements.put("ITEMNAME", itemName);
        replacements.put("PRIMARYKEYCOLUMN", GeneratorUtils.firstCharConvert(primaryKeyColumn, false));
        replacements.put("PRIMARYKEYCOLUMN0", GeneratorUtils.firstCharConvert(primaryKeyColumn, true));
        replacements.put("MAJORCOLUMN", GeneratorUtils.firstCharConvert(majorColumn, false));
        replacements.put("MAJORCOLUMN0", GeneratorUtils.firstCharConvert(majorColumn, true));
        replacements.put("ENTITYPREFIX", constantPrefix);
        replacements.put("APIMODULE", normalizedFrontendApiModule);
        replacements.put("SEARCHSTATECONTENT", buildSearchStateContent());
        replacements.put("FORMSTATECONTENT", buildFormStateContent());
        replacements.put("FIELDOPTIONSCONTENT", buildFieldOptionsContent());
        replacements.put("SEARCHPARAMSCONTENT", buildSearchParamsContent());
        replacements.put("SAVEPAYLOADCONTENT", buildSavePayloadContent());
        replacements.put("DIALOGMODELCONTENT", buildDialogModelContent());
        replacements.put("NORMALIZEROWCONTENT", buildNormalizeRowContent());
        replacements.put("REPORTCOLUMNSCONTENT", buildReportColumnsContent());
        replacements.put("REPORTSOURCEALIASCONTENT", buildReportSourceAliasContent());
        replacements.put("SEARCHITEMSCONTENT", buildSearchItemsContent());
        replacements.put("FORMITEMSCONTENT", buildFormItemsContent());
        replacements.put("RULESCONTENT", buildRulesContent());
        replacements.put("TABLECOLUMNSCONTENT", buildTableColumnsContent());
        replacements.put("EXPORTROWCONTENT", buildExportRowContent());
        replacements.put("SAVEINITCONTENT", buildSaveInitContent());
        replacements.put("UPDATEINITCONTENT", buildUpdateInitContent());
        return replacements;
    }
    private void writeFile(String content, String directory, String fileName, String templateName) throws IOException {
        File codeDirectory = new File(directory);
        if (!codeDirectory.exists()) {
            codeDirectory.mkdirs();
        }
        File writerFile = new File(directory + fileName);
        if (writerFile.exists()) {
            System.out.println(fullEntityName + templateName + " 源文件已经存在创建失败!");
            return;
        }
        writerFile.createNewFile();
        try (BufferedWriter writer = new BufferedWriter(
                new OutputStreamWriter(new FileOutputStream(writerFile), StandardCharsets.UTF_8))) {
            writer.write(content);
            writer.flush();
        }
        System.out.println(fullEntityName + templateName + " 源文件创建成功!");
    }
    private String resolvePrimaryKeyColumn() {
        for (Column column : columns) {
            if (column.isPrimaryKey() || column.isMainKey()) {
                return column.getHumpName();
            }
        }
        return "id";
    }
    private String resolveMajorColumn() {
        for (Column column : columns) {
            if (column.isMajor()) {
                return column.getHumpName();
            }
        }
        for (String preferred : Arrays.asList("name", "code", "title", "uuid")) {
            Column column = findColumn(preferred);
            if (column != null) {
                return column.getHumpName();
            }
        }
        for (Column column : columns) {
            if (!MANAGED_FIELDS.contains(column.getHumpName()) && !column.isPrimaryKey()) {
                return column.getHumpName();
            }
        }
        return primaryKeyColumn;
    }
    private Column findColumn(String humpName) {
        for (Column column : columns) {
            if (humpName.equals(column.getHumpName())) {
                return column;
            }
        }
        return null;
    }
    private boolean hasColumn(String humpName) {
        return findColumn(humpName) != null;
    }
    private boolean isManagedColumn(Column column) {
        return MANAGED_FIELDS.contains(column.getHumpName());
    }
    private boolean isSearchExcludedColumn(Column column) {
        return SEARCH_EXCLUDED_FIELDS.contains(column.getHumpName());
    }
    private boolean isBooleanColumn(Column column) {
        return "Boolean".equals(column.getType());
    }
    private boolean isNumericColumn(Column column) {
        return "Short".equals(column.getType())
                || "Integer".equals(column.getType())
                || "Long".equals(column.getType())
                || "Double".equals(column.getType());
    }
    private boolean isDateColumn(Column column) {
        return "Date".equals(column.getType());
    }
    private boolean hasEnumOptions(Column column) {
        return !Cools.isEmpty(column.getEnums());
    }
    private boolean hasForeignDisplay(Column column) {
        return !Cools.isEmpty(column.getForeignKeyMajor());
    }
    private boolean isStatusColumn(Column column) {
        return "status".equals(column.getHumpName());
    }
    private boolean isMemoColumn(Column column) {
        return "memo".equals(column.getHumpName());
    }
    private boolean isSelectableColumn(Column column) {
        return isBooleanColumn(column) || hasEnumOptions(column);
    }
    private boolean isDisplayTextColumn(Column column) {
        return isBooleanColumn(column) || isDateColumn(column) || hasEnumOptions(column) || hasForeignDisplay(column);
    }
    private List<Column> getSearchColumns() {
        List<Column> result = new ArrayList<>();
        for (Column column : columns) {
            if (column.isPrimaryKey() || isSearchExcludedColumn(column) || isDateColumn(column)) {
                continue;
            }
            result.add(column);
        }
        return result;
    }
    private List<Column> getFormColumns() {
        List<Column> result = new ArrayList<>();
        for (Column column : columns) {
            if (column.isPrimaryKey() || isManagedColumn(column)) {
                continue;
            }
            result.add(column);
        }
        return result;
    }
    private List<Column> getListColumns() {
        List<Column> result = new ArrayList<>();
        for (Column column : columns) {
            if (column.isPrimaryKey()) {
                continue;
            }
            if ("deleted".equals(column.getHumpName()) || "tenantId".equals(column.getHumpName())) {
                continue;
            }
            result.add(column);
        }
        return result;
    }
    private List<Column> getReportColumns() {
        return getListColumns();
    }
    private List<Column> getPayloadColumns() {
        List<Column> result = new ArrayList<>();
        Column primaryColumn = findColumn(primaryKeyColumn);
        if (primaryColumn != null) {
            result.add(primaryColumn);
        }
        Column versionColumn = findColumn("version");
        if (versionColumn != null) {
            result.add(versionColumn);
        }
        result.addAll(getFormColumns());
        return result;
    }
    private String buildSearchStateContent() {
        StringBuilder sb = new StringBuilder();
        for (Column column : getSearchColumns()) {
            sb.append("    ").append(column.getHumpName()).append(": '',\n");
        }
        return trimTrailingLineBreak(sb);
    }
    private String buildFormStateContent() {
        StringBuilder sb = new StringBuilder();
        for (Column column : getPayloadColumns()) {
            sb.append("    ")
                    .append(column.getHumpName())
                    .append(": ")
                    .append(resolveFormDefaultValue(column))
                    .append(",\n");
        }
        return trimTrailingLineBreak(sb);
    }
    private String buildFieldOptionsContent() {
        StringBuilder sb = new StringBuilder();
        sb.append("{\n");
        boolean hasOptions = false;
        for (Column column : columns) {
            if (!isSelectableColumn(column)) {
                continue;
            }
            hasOptions = true;
            sb.append("  ").append(column.getHumpName()).append(": [\n");
            for (Map<String, Object> option : buildColumnOptions(column)) {
                sb.append("    { label: '")
                        .append(escapeJs(String.valueOf(option.get("label"))))
                        .append("', value: ")
                        .append(option.get("value"))
                        .append(" },\n");
            }
            sb.append("  ],\n");
        }
        if (!hasOptions) {
            return "{}";
        }
        sb.append("}");
        return sb.toString();
    }
    private String buildSearchParamsContent() {
        StringBuilder sb = new StringBuilder();
        for (Column column : getSearchColumns()) {
            sb.append("    ")
                    .append(column.getHumpName())
                    .append(": ")
                    .append(resolveParamNormalizer("params", column))
                    .append(",\n");
        }
        return trimTrailingLineBreak(sb);
    }
    private String buildSavePayloadContent() {
        StringBuilder sb = new StringBuilder();
        for (Column column : getPayloadColumns()) {
            sb.append(resolvePayloadLine("formData", column));
        }
        return trimTrailingLineBreak(sb);
    }
    private String buildDialogModelContent() {
        StringBuilder sb = new StringBuilder();
        for (Column column : getPayloadColumns()) {
            sb.append("    ")
                    .append(column.getHumpName())
                    .append(": ")
                    .append(resolveDialogValue(column))
                    .append(",\n");
        }
        return trimTrailingLineBreak(sb);
    }
    private String buildNormalizeRowContent() {
        StringBuilder sb = new StringBuilder();
        Column statusColumn = findColumn("status");
        if (statusColumn != null) {
            sb.append("    statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,\n")
                    .append("    statusText: normalizeText(record.statusText || record.status$ || statusMeta.text),\n")
                    .append("    statusType: statusMeta.type,\n");
        }
        for (Column column : getReportColumns()) {
            if (isStatusColumn(column)) {
                continue;
            }
            if (isBooleanColumn(column)) {
                sb.append("    ")
                        .append(column.getHumpName())
                        .append("Text: formatBooleanText(toOptionalBoolean(record.")
                        .append(column.getHumpName())
                        .append(")),\n");
                continue;
            }
            if (isDisplayTextColumn(column)) {
                sb.append("    ")
                        .append(column.getHumpName())
                        .append("Text: normalizeText(record.")
                        .append(column.getHumpName())
                        .append("$ || record.")
                        .append(column.getHumpName())
                        .append("Text || record.")
                        .append(column.getHumpName())
                        .append("),\n");
            }
        }
        return trimTrailingLineBreak(sb);
    }
    private String buildReportColumnsContent() {
        StringBuilder sb = new StringBuilder();
        for (Column column : getReportColumns()) {
            sb.append("  { source: '")
                    .append(resolveReportSource(column))
                    .append("', label: '")
                    .append(escapeJs(resolveFieldLabel(column)))
                    .append("' },\n");
        }
        return trimTrailingLineBreak(sb);
    }
    private String buildReportSourceAliasContent() {
        Column statusColumn = findColumn("status");
        if (statusColumn == null) {
            return "";
        }
        return "  status: 'statusText'";
    }
    private String buildSearchItemsContent() {
        StringBuilder sb = new StringBuilder();
        for (Column column : getSearchColumns()) {
            if (isSelectableColumn(column)) {
                sb.append("    createSelectSearchItem('")
                        .append(escapeJs(resolveFieldLabel(column)))
                        .append("', '")
                        .append(column.getHumpName())
                        .append("', '请选择")
                        .append(escapeJs(resolveFieldLabel(column)))
                        .append("', get")
                        .append(fullEntityName)
                        .append("FieldOptions('")
                        .append(column.getHumpName())
                        .append("')),\n");
            } else {
                sb.append("    createInputSearchItem('")
                        .append(escapeJs(resolveFieldLabel(column)))
                        .append("', '")
                        .append(column.getHumpName())
                        .append("', '请输入")
                        .append(escapeJs(resolveFieldLabel(column)))
                        .append("'),\n");
            }
        }
        return trimTrailingLineBreak(sb);
    }
    private String buildFormItemsContent() {
        StringBuilder sb = new StringBuilder();
        for (Column column : getFormColumns()) {
            String label = escapeJs(resolveFieldLabel(column));
            String key = column.getHumpName();
            if (isSelectableColumn(column)) {
                sb.append("    createSelectFormItem('")
                        .append(label)
                        .append("', '")
                        .append(key)
                        .append("', '请选择")
                        .append(label)
                        .append("', get")
                        .append(fullEntityName)
                        .append("FieldOptions('")
                        .append(key)
                        .append("')),\n");
                continue;
            }
            if (isMemoColumn(column) || (column.getLength() != null && column.getLength() > 255)) {
                sb.append("    createInputFormItem('")
                        .append(label)
                        .append("', '")
                        .append(key)
                        .append("', '请输入")
                        .append(label)
                        .append("', { type: 'textarea', rows: 3 }, { span: 24 }),\n");
                continue;
            }
            if (isNumericColumn(column)) {
                sb.append("    createInputFormItem('")
                        .append(label)
                        .append("', '")
                        .append(key)
                        .append("', '请输入")
                        .append(label)
                        .append("', { type: 'number' }),\n");
                continue;
            }
            sb.append("    createInputFormItem('")
                    .append(label)
                    .append("', '")
                    .append(key)
                    .append("', '请输入")
                    .append(label)
                    .append("'),\n");
        }
        return trimTrailingLineBreak(sb);
    }
    private String buildRulesContent() {
        StringBuilder sb = new StringBuilder();
        for (Column column : getFormColumns()) {
            if (!column.isNotNull()) {
                continue;
            }
            String label = escapeJs(resolveFieldLabel(column));
            String message = (isSelectableColumn(column) ? "请选择" : "请输入") + label;
            String trigger = isSelectableColumn(column) ? "change" : "blur";
            sb.append("    ")
                    .append(column.getHumpName())
                    .append(": [{ required: true, message: '")
                    .append(message)
                    .append("', trigger: '")
                    .append(trigger)
                    .append("' }],\n");
        }
        return trimTrailingLineBreak(sb);
    }
    private String buildTableColumnsContent() {
        StringBuilder sb = new StringBuilder();
        for (Column column : getListColumns()) {
            if (isStatusColumn(column)) {
                sb.append("    createTagColumn('status', '")
                        .append(escapeJs(resolveFieldLabel(column)))
                        .append("', 120, (row) => get")
                        .append(fullEntityName)
                        .append("StatusMeta(row.statusBool ?? row.status)),\n");
                continue;
            }
            if (isNumericColumn(column)) {
                sb.append("    createNumberColumn('")
                        .append(column.getHumpName())
                        .append("', '")
                        .append(escapeJs(resolveFieldLabel(column)))
                        .append("', 120),\n");
                continue;
            }
            if (isDisplayTextColumn(column)) {
                sb.append("    createTextColumn('")
                        .append(column.getHumpName())
                        .append("Text', '")
                        .append(escapeJs(resolveFieldLabel(column)))
                        .append("', ")
                        .append(resolveTextColumnWidth(column))
                        .append("),\n");
                continue;
            }
            sb.append("    createTextColumn('")
                    .append(column.getHumpName())
                    .append("', '")
                    .append(escapeJs(resolveFieldLabel(column)))
                    .append("', ")
                    .append(resolveTextColumnWidth(column))
                    .append("),\n");
        }
        return trimTrailingLineBreak(sb);
    }
    private String buildExportRowContent() {
        StringBuilder sb = new StringBuilder();
        for (Column column : getReportColumns()) {
            sb.append("            row.put(\"")
                    .append(resolveReportSource(column))
                    .append("\", ")
                    .append(resolveExportExpression(column))
                    .append(");\n");
        }
        return trimTrailingLineBreak(sb);
    }
    private String buildSaveInitContent() {
        StringBuilder sb = new StringBuilder();
        if (hasColumn("createBy")) {
            sb.append("        ").append(simpleEntityName).append(".setCreateBy(getLoginUserId());\n");
        }
        if (hasColumn("createTime")) {
            sb.append("        ").append(simpleEntityName).append(".setCreateTime(new Date());\n");
        }
        if (hasColumn("updateBy")) {
            sb.append("        ").append(simpleEntityName).append(".setUpdateBy(getLoginUserId());\n");
        }
        if (hasColumn("updateTime")) {
            sb.append("        ").append(simpleEntityName).append(".setUpdateTime(new Date());\n");
        }
        return trimTrailingLineBreak(sb);
    }
    private String buildUpdateInitContent() {
        StringBuilder sb = new StringBuilder();
        if (hasColumn("updateBy")) {
            sb.append("        ").append(simpleEntityName).append(".setUpdateBy(getLoginUserId());\n");
        }
        if (hasColumn("updateTime")) {
            sb.append("        ").append(simpleEntityName).append(".setUpdateTime(new Date());\n");
        }
        return trimTrailingLineBreak(sb);
    }
    private List<Map<String, Object>> buildColumnOptions(Column column) {
        List<Map<String, Object>> options = new ArrayList<>();
        if (isBooleanColumn(column) && Cools.isEmpty(column.getEnums())) {
            options.add(buildOption("是", "true"));
            options.add(buildOption("否", "false"));
            return options;
        }
        if (Cools.isEmpty(column.getEnums())) {
            return options;
        }
        for (Map<String, Object> item : column.getEnums()) {
            for (Map.Entry<String, Object> entry : item.entrySet()) {
                options.add(buildOption(String.valueOf(entry.getValue()), toJsValue(column, String.valueOf(entry.getKey()))));
            }
        }
        return options;
    }
    private Map<String, Object> buildOption(String label, String jsValue) {
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("label", label);
        option.put("value", jsValue);
        return option;
    }
    private String resolveFormDefaultValue(Column column) {
        if (column == null) {
            return "void 0";
        }
        if (isStatusColumn(column) && isSelectableColumn(column)) {
            List<Map<String, Object>> options = buildColumnOptions(column);
            if (!options.isEmpty()) {
                return String.valueOf(options.get(0).get("value"));
            }
        }
        if (isBooleanColumn(column) || isNumericColumn(column)) {
            return "void 0";
        }
        return "''";
    }
    private String resolveParamNormalizer(String sourceName, Column column) {
        String field = sourceName + "." + column.getHumpName();
        if (isBooleanColumn(column)) {
            return "toOptionalBoolean(" + field + ")";
        }
        if (isNumericColumn(column)) {
            return "toOptionalNumber(" + field + ")";
        }
        return "normalizeText(" + field + ")";
    }
    private String resolvePayloadLine(String sourceName, Column column) {
        String field = sourceName + "." + column.getHumpName();
        if (isBooleanColumn(column)) {
            return "    ...(hasValue(" + field + ") ? { " + column.getHumpName() + ": toOptionalBoolean(" + field + ") } : {}),\n";
        }
        if (isNumericColumn(column)) {
            return "    ...buildNumberField('" + column.getHumpName() + "', " + field + "),\n";
        }
        return "    " + column.getHumpName() + ": normalizeText(" + field + ") || '',\n";
    }
    private String resolveDialogValue(Column column) {
        String field = "record." + column.getHumpName();
        if (isStatusColumn(column) && isSelectableColumn(column)) {
            List<Map<String, Object>> options = buildColumnOptions(column);
            String fallback = options.isEmpty() ? "void 0" : String.valueOf(options.get(0).get("value"));
            if (isBooleanColumn(column)) {
                return "hasValue(" + field + ") ? toOptionalBoolean(" + field + ") : " + fallback;
            }
            if (isNumericColumn(column)) {
                return "hasValue(" + field + ") ? toOptionalNumber(" + field + ") : " + fallback;
            }
            return "normalizeText(" + field + ") || " + fallback;
        }
        if (isBooleanColumn(column)) {
            return "toOptionalBoolean(" + field + ")";
        }
        if (isNumericColumn(column)) {
            return "toOptionalNumber(" + field + ")";
        }
        return "normalizeText(" + field + " || '')";
    }
    private String resolveFieldLabel(Column column) {
        if (column == null || Cools.isEmpty(column.getComment())) {
            return column == null ? "" : column.getHumpName();
        }
        return column.getComment().trim();
    }
    private int resolveTextColumnWidth(Column column) {
        if (isMemoColumn(column)) {
            return 220;
        }
        if (isDateColumn(column)) {
            return 180;
        }
        return 160;
    }
    private String resolveReportSource(Column column) {
        if (isStatusColumn(column) || isDisplayTextColumn(column)) {
            return column.getHumpName() + "Text";
        }
        return column.getHumpName();
    }
    private String resolveExportExpression(Column column) {
        String getter = "record." + getterName(column);
        if (isBooleanColumn(column)) {
            return "Boolean.TRUE.equals(" + getter + "()) ? \"是\" : Boolean.FALSE.equals(" + getter + "()) ? \"否\" : \"\"";
        }
        if (isStatusColumn(column) || isDisplayTextColumn(column)) {
            return getter + "$()";
        }
        return getter + "()";
    }
    private String getterName(Column column) {
        return "get" + GeneratorUtils.firstCharConvert(column.getHumpName(), false);
    }
    private String toJsValue(Column column, String rawValue) {
        if (isBooleanColumn(column)) {
            if ("1".equals(rawValue) || "true".equalsIgnoreCase(rawValue)) {
                return "true";
            }
            if ("0".equals(rawValue) || "false".equalsIgnoreCase(rawValue)) {
                return "false";
            }
        }
        if (isNumericColumn(column)) {
            return rawValue;
        }
        return "'" + escapeJs(rawValue) + "'";
    }
    private String trimTrailingLineBreak(StringBuilder sb) {
        if (sb.length() == 0) {
            return "";
        }
        while (sb.length() > 0 && (sb.charAt(sb.length() - 1) == '\n' || sb.charAt(sb.length() - 1) == '\r')) {
            sb.deleteCharAt(sb.length() - 1);
        }
        if (sb.length() > 0 && sb.charAt(sb.length() - 1) == ',') {
            sb.deleteCharAt(sb.length() - 1);
        }
        return sb.toString();
    }
    private String safeText(String value) {
        return value == null ? "" : value.trim();
    }
    private String escapeJs(String value) {
        return value
                .replace("\\", "\\\\")
                .replace("'", "\\'");
    }
    private String humpToKebab(String value) {
        if (Cools.isEmpty(value)) {
            return "";
        }
        return value.replaceAll("([a-z0-9])([A-Z])", "$1-$2").toLowerCase();
    }
    private String ensureTrailingSlash(String prefix) {
        if (Cools.isEmpty(prefix)) {
            return "";
        }
        return prefix.endsWith("/") ? prefix : prefix + "/";
    }
    private String normalizePath(String value) {
        if (Cools.isEmpty(value)) {
            return "";
        }
        String normalized = value.replace("\\", "/");
        while (normalized.startsWith("/")) {
            normalized = normalized.substring(1);
        }
        while (normalized.endsWith("/")) {
            normalized = normalized.substring(0, normalized.length() - 1);
        }
        return normalized;
    }
    private String normalizeApiModule(String value) {
        String normalized = normalizePath(value);
        if (normalized.endsWith(".js")) {
            return normalized.substring(0, normalized.length() - 3);
        }
        return normalized;
    }
}
rsf-framework/src/main/resources/templates/rsf-design/Api.txt
New file
@@ -0,0 +1,84 @@
import request from '@/utils/http'
function normalizeIds(ids) {
  if (Array.isArray(ids)) {
    return ids
      .map((id) => String(id).trim())
      .filter(Boolean)
      .join(',')
  }
  if (ids === null || ids === undefined) {
    return ''
  }
  return String(ids).trim()
}
function normalizeText(value) {
  return typeof value === 'string' ? value.trim() : value
}
export function fetch@{ENTITYNAME}Page(params = {}) {
  return request.post({
    url: '/@{SIMPLEENTITYNAME}/page',
    params
  })
}
export function fetch@{ENTITYNAME}List(params = {}) {
  return request.post({
    url: '/@{SIMPLEENTITYNAME}/list',
    params
  })
}
export function fetchGet@{ENTITYNAME}Detail(id) {
  return request.get({
    url: `/@{SIMPLEENTITYNAME}/${id}`
  })
}
export function fetchGet@{ENTITYNAME}Many(ids) {
  return request.post({
    url: `/@{SIMPLEENTITYNAME}/many/${normalizeIds(ids)}`
  })
}
export function fetchSave@{ENTITYNAME}(params = {}) {
  return request.post({
    url: '/@{SIMPLEENTITYNAME}/save',
    params
  })
}
export function fetchUpdate@{ENTITYNAME}(params = {}) {
  return request.post({
    url: '/@{SIMPLEENTITYNAME}/update',
    params
  })
}
export function fetchDelete@{ENTITYNAME}(ids) {
  return request.post({
    url: `/@{SIMPLEENTITYNAME}/remove/${normalizeIds(ids)}`
  })
}
export function fetch@{ENTITYNAME}Query(condition = '') {
  return request.post({
    url: '/@{SIMPLEENTITYNAME}/query',
    params: {
      condition: normalizeText(condition)
    }
  })
}
export async function fetchExport@{ENTITYNAME}Report(payload = {}, options = {}) {
  return fetch(`${import.meta.env.VITE_API_URL}/@{SIMPLEENTITYNAME}/export`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      ...(options.headers || {})
    },
    body: JSON.stringify(payload)
  })
}
rsf-framework/src/main/resources/templates/rsf-design/Controller.txt
New file
@@ -0,0 +1,140 @@
package @{COMPANYNAME}.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.server.common.annotation.OperationLog;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.domain.KeyValVo;
import com.vincent.rsf.server.common.domain.PageParam;
import com.vincent.rsf.server.common.service.ListExportHandler;
import com.vincent.rsf.server.common.service.ListExportService;
import com.vincent.rsf.server.common.utils.ExcelUtil;
import @{COMPANYNAME}.entity.@{ENTITYNAME};
import @{COMPANYNAME}.service.@{ENTITYNAME}Service;
import com.vincent.rsf.server.system.controller.BaseController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@RestController
public class @{ENTITYNAME}Controller extends BaseController {
    @Autowired
    private @{ENTITYNAME}Service @{SIMPLEENTITYNAME}Service;
    @Autowired
    private ListExportService listExportService;
    private final ListExportHandler<@{ENTITYNAME}, BaseParam> @{SIMPLEENTITYNAME}ExportHandler =
            new ListExportHandler<@{ENTITYNAME}, BaseParam>() {
                @Override
                public List<@{ENTITYNAME}> listByIds(List<Long> ids) {
                    return @{SIMPLEENTITYNAME}Service.listByIds(ids);
                }
                @Override
                public List<@{ENTITYNAME}> listByFilter(Map<String, Object> sanitizedMap, BaseParam baseParam) {
                    PageParam<@{ENTITYNAME}, BaseParam> pageParam = new PageParam<>(baseParam, @{ENTITYNAME}.class);
                    return @{SIMPLEENTITYNAME}Service.list(pageParam.buildWrapper(true));
                }
                @Override
                public Map<String, Object> toExportRow(@{ENTITYNAME} record, List<ExcelUtil.ExportColumn> columns) {
                    Map<String, Object> row = new LinkedHashMap<>();
@{EXPORTROWCONTENT}
                    return row;
                }
                @Override
                public String defaultReportTitle() {
                    return "@{TABLEDESC}报表";
                }
            };
    @PreAuthorize("hasAuthority('@{ITEMNAME}:@{SIMPLEENTITYNAME}:list')")
    @PostMapping("/@{SIMPLEENTITYNAME}/page")
    public R page(@RequestBody Map<String, Object> map) {
        BaseParam baseParam = buildParam(map, BaseParam.class);
        PageParam<@{ENTITYNAME}, BaseParam> pageParam = new PageParam<>(baseParam, @{ENTITYNAME}.class);
        return R.ok().add(@{SIMPLEENTITYNAME}Service.page(pageParam, pageParam.buildWrapper(true)));
    }
    @PreAuthorize("hasAuthority('@{ITEMNAME}:@{SIMPLEENTITYNAME}:list')")
    @PostMapping("/@{SIMPLEENTITYNAME}/list")
    public R list(@RequestBody Map<String, Object> map) {
        return R.ok().add(@{SIMPLEENTITYNAME}Service.list());
    }
    @PreAuthorize("hasAuthority('@{ITEMNAME}:@{SIMPLEENTITYNAME}:list')")
    @PostMapping({"/@{SIMPLEENTITYNAME}/many/{ids}", "/@{SIMPLEENTITYNAME}s/many/{ids}"})
    public R many(@PathVariable Long[] ids) {
        return R.ok().add(@{SIMPLEENTITYNAME}Service.listByIds(Arrays.asList(ids)));
    }
    @PreAuthorize("hasAuthority('@{ITEMNAME}:@{SIMPLEENTITYNAME}:list')")
    @GetMapping("/@{SIMPLEENTITYNAME}/{id}")
    public R get(@PathVariable("id") Long id) {
        return R.ok().add(@{SIMPLEENTITYNAME}Service.getById(id));
    }
    @PreAuthorize("hasAuthority('@{ITEMNAME}:@{SIMPLEENTITYNAME}:save')")
    @OperationLog("Create @{TABLEDESC}")
    @PostMapping("/@{SIMPLEENTITYNAME}/save")
    public R save(@RequestBody @{ENTITYNAME} @{SIMPLEENTITYNAME}) {
@{SAVEINITCONTENT}
        if (!@{SIMPLEENTITYNAME}Service.save(@{SIMPLEENTITYNAME})) {
            return R.error("Save Fail");
        }
        return R.ok("Save Success").add(@{SIMPLEENTITYNAME});
    }
    @PreAuthorize("hasAuthority('@{ITEMNAME}:@{SIMPLEENTITYNAME}:update')")
    @OperationLog("Update @{TABLEDESC}")
    @PostMapping("/@{SIMPLEENTITYNAME}/update")
    public R update(@RequestBody @{ENTITYNAME} @{SIMPLEENTITYNAME}) {
@{UPDATEINITCONTENT}
        if (!@{SIMPLEENTITYNAME}Service.updateById(@{SIMPLEENTITYNAME})) {
            return R.error("Update Fail");
        }
        return R.ok("Update Success").add(@{SIMPLEENTITYNAME});
    }
    @PreAuthorize("hasAuthority('@{ITEMNAME}:@{SIMPLEENTITYNAME}:remove')")
    @OperationLog("Delete @{TABLEDESC}")
    @PostMapping("/@{SIMPLEENTITYNAME}/remove/{ids}")
    public R remove(@PathVariable Long[] ids) {
        if (!@{SIMPLEENTITYNAME}Service.removeByIds(Arrays.asList(ids))) {
            return R.error("Delete Fail");
        }
        return R.ok("Delete Success").add(ids);
    }
    @PreAuthorize("hasAuthority('@{ITEMNAME}:@{SIMPLEENTITYNAME}:list')")
    @PostMapping("/@{SIMPLEENTITYNAME}/query")
    public R query(@RequestParam(required = false) String condition) {
        List<KeyValVo> vos = new ArrayList<>();
        LambdaQueryWrapper<@{ENTITYNAME}> wrapper = new LambdaQueryWrapper<>();
        if (!Cools.isEmpty(condition)) {
            wrapper.like(@{ENTITYNAME}::get@{MAJORCOLUMN}, condition);
        }
        @{SIMPLEENTITYNAME}Service.page(new Page<>(1, 30), wrapper).getRecords().forEach(
                item -> vos.add(new KeyValVo(item.get@{PRIMARYKEYCOLUMN}(), item.get@{MAJORCOLUMN}()))
        );
        return R.ok().add(vos);
    }
    @PreAuthorize("hasAuthority('@{ITEMNAME}:@{SIMPLEENTITYNAME}:list')")
    @PostMapping("/@{SIMPLEENTITYNAME}/export")
    public void export(@RequestBody Map<String, Object> map, HttpServletResponse response) throws Exception {
        listExportService.export(map, exportMap -> buildParam(exportMap, BaseParam.class), @{SIMPLEENTITYNAME}ExportHandler, response);
    }
}
rsf-framework/src/main/resources/templates/rsf-design/EditDialog.txt
New file
@@ -0,0 +1,136 @@
<template>
  <ElDialog
    :title="dialogTitle"
    :model-value="visible"
    width="820px"
    align-center
    @update:model-value="handleCancel"
    @closed="handleClosed"
  >
    <ArtForm
      ref="formRef"
      v-model="form"
      :items="formItems"
      :rules="rules"
      :span="12"
      :gutter="20"
      label-width="110px"
      :show-reset="false"
      :show-submit="false"
    />
    <template #footer>
      <span class="dialog-footer">
        <ElButton @click="handleCancel">取消</ElButton>
        <ElButton type="primary" @click="handleSubmit">确定</ElButton>
      </span>
    </template>
  </ElDialog>
</template>
<script setup>
  import ArtForm from '@/components/core/forms/art-form/index.vue'
  import {
    build@{ENTITYNAME}DialogModel,
    create@{ENTITYNAME}FormState,
    get@{ENTITYNAME}FieldOptions
  } from '../@{SIMPLEENTITYNAME}Page.helpers'
  const props = defineProps({
    visible: { required: false, default: false },
    dialogType: { required: false, default: 'add' },
    record: { required: false, default: () => ({}) }
  })
  const emit = defineEmits(['update:visible', 'submit'])
  const formRef = ref()
  const form = reactive(create@{ENTITYNAME}FormState())
  const isEdit = computed(() => props.dialogType === 'edit')
  const dialogTitle = computed(() => (isEdit.value ? '编辑@{TABLEDESC}' : '新增@{TABLEDESC}'))
  const rules = computed(() => ({
@{RULESCONTENT}
  }))
  function createInputFormItem(label, key, placeholder, extraProps = {}, extraConfig = {}) {
    return {
      label,
      key,
      type: 'input',
      props: {
        placeholder,
        clearable: true,
        ...extraProps
      },
      ...extraConfig
    }
  }
  function createSelectFormItem(label, key, placeholder, options, extraProps = {}, extraConfig = {}) {
    return {
      label,
      key,
      type: 'select',
      props: {
        placeholder,
        clearable: true,
        options,
        ...extraProps
      },
      ...extraConfig
    }
  }
  const formItems = computed(() => [
@{FORMITEMSCONTENT}
  ])
  function resetForm() {
    Object.assign(form, create@{ENTITYNAME}FormState())
    formRef.value?.clearValidate?.()
  }
  function loadFormData() {
    Object.assign(form, build@{ENTITYNAME}DialogModel(props.record))
  }
  async function handleSubmit() {
    if (!formRef.value) return
    try {
      await formRef.value.validate()
      emit('submit', { ...form })
    } catch {
      return
    }
  }
  function handleCancel() {
    emit('update:visible', false)
  }
  function handleClosed() {
    resetForm()
  }
  watch(
    () => props.visible,
    (visible) => {
      if (visible) {
        loadFormData()
        nextTick(() => formRef.value?.clearValidate?.())
      }
    },
    { immediate: true }
  )
  watch(
    () => props.record,
    () => {
      if (props.visible) {
        loadFormData()
      }
    },
    { deep: true }
  )
</script>
rsf-framework/src/main/resources/templates/rsf-design/Index.txt
New file
@@ -0,0 +1,240 @@
<template>
  <div class="art-full-height">
    <@{ENTITYNAME}Search
      v-show="showSearchBar"
      v-model="searchForm"
      @search="handleSearch"
      @reset="handleReset"
    />
    <ElCard class="art-table-card" :style="{ 'margin-top': showSearchBar ? '12px' : '0' }">
      <ArtTableHeader
        v-model:columns="columnChecks"
        v-model:showSearchBar="showSearchBar"
        :loading="loading"
        @refresh="refreshData"
      >
        <template #left>
          <ElSpace wrap>
            <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>新增@{TABLEDESC}</ElButton>
            <ElButton
              v-auth="'delete'"
              type="danger"
              :disabled="selectedRows.length === 0"
              @click="handleBatchDelete"
              v-ripple
            >
              批量删除
            </ElButton>
            <span v-auth="'query'" class="inline-flex">
              <ListExportPrint
                :preview-visible="previewVisible"
                @update:previewVisible="handlePreviewVisibleChange"
                :report-title="reportTitle"
                :selected-rows="selectedRows"
                :query-params="reportQueryParams"
                :columns="reportColumns"
                :preview-rows="previewRows"
                :preview-meta="resolvedPreviewMeta"
                :total="pagination.total"
                :disabled="loading"
                @export="handleExport"
                @print="handlePrint"
              />
            </span>
          </ElSpace>
        </template>
      </ArtTableHeader>
      <ArtTable
        :loading="loading"
        :data="data"
        :columns="columns"
        :pagination="pagination"
        @selection-change="handleSelectionChange"
        @pagination:size-change="handleSizeChange"
        @pagination:current-change="handleCurrentChange"
      />
    </ElCard>
    <@{ENTITYNAME}EditDialog
      v-model:visible="dialogVisible"
      :dialog-type="dialogType"
      :record="currentRecordData"
      @submit="handleDialogSubmit"
    />
  </div>
</template>
<script setup>
  import { useUserStore } from '@/store/modules/user'
  import {
    fetchDelete@{ENTITYNAME},
    fetchExport@{ENTITYNAME}Report,
    fetchGet@{ENTITYNAME}Many,
    fetch@{ENTITYNAME}Page,
    fetchSave@{ENTITYNAME},
    fetchUpdate@{ENTITYNAME}
  } from '@/api/@{APIMODULE}'
  import { useTable } from '@/hooks/core/useTable'
  import { useCrudPage } from '@/views/system/common/useCrudPage'
  import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
  import { defaultResponseAdapter } from '@/utils/table/tableUtils'
  import ListExportPrint from '@/components/biz/list-export-print/index.vue'
  import @{ENTITYNAME}Search from './modules/@{KEBABENTITYNAME}-search.vue'
  import @{ENTITYNAME}EditDialog from './modules/@{KEBABENTITYNAME}-edit-dialog.vue'
  import { create@{ENTITYNAME}TableColumns } from './@{SIMPLEENTITYNAME}Table.columns'
  import {
    build@{ENTITYNAME}DialogModel,
    build@{ENTITYNAME}PageQueryParams,
    build@{ENTITYNAME}PrintRows,
    build@{ENTITYNAME}ReportMeta,
    build@{ENTITYNAME}SavePayload,
    build@{ENTITYNAME}SearchParams,
    create@{ENTITYNAME}SearchState,
    get@{ENTITYNAME}PaginationKey,
    normalize@{ENTITYNAME}ListRow,
    @{ENTITYPREFIX}_REPORT_STYLE,
    @{ENTITYPREFIX}_REPORT_TITLE,
    resolve@{ENTITYNAME}ReportColumns
  } from './@{SIMPLEENTITYNAME}Page.helpers'
  defineOptions({ name: '@{ENTITYNAME}' })
  const userStore = useUserStore()
  const searchForm = ref(create@{ENTITYNAME}SearchState())
  const showSearchBar = ref(false)
  const reportTitle = @{ENTITYPREFIX}_REPORT_TITLE
  const reportQueryParams = computed(() => build@{ENTITYNAME}SearchParams(searchForm.value))
  let handleEditAction = null
  let handleDeleteAction = null
  const {
    columns,
    columnChecks,
    data,
    loading,
    pagination,
    getData,
    replaceSearchParams,
    resetSearchParams,
    handleSizeChange,
    handleCurrentChange,
    refreshData,
    refreshCreate,
    refreshUpdate,
    refreshRemove
  } = useTable({
    core: {
      apiFn: fetch@{ENTITYNAME}Page,
      apiParams: build@{ENTITYNAME}PageQueryParams(searchForm.value),
      paginationKey: get@{ENTITYNAME}PaginationKey(),
      columnsFactory: () =>
        create@{ENTITYNAME}TableColumns({
          handleEdit: (row) => handleEditAction?.(row),
          handleDelete: (row) => handleDeleteAction?.(row)
        })
    },
    transform: {
      dataTransformer: (records) => {
        if (!Array.isArray(records)) {
          return []
        }
        return records.map((item) => normalize@{ENTITYNAME}ListRow(item))
      }
    }
  })
  const {
    dialogVisible,
    dialogType,
    currentRecord: currentRecordData,
    selectedRows,
    handleSelectionChange,
    showDialog,
    handleDialogSubmit,
    handleDelete,
    handleBatchDelete
  } = useCrudPage({
    createEmptyModel: () => build@{ENTITYNAME}DialogModel(),
    buildEditModel: (record) => build@{ENTITYNAME}DialogModel(record),
    buildSavePayload: (formData) => build@{ENTITYNAME}SavePayload(formData),
    saveRequest: fetchSave@{ENTITYNAME},
    updateRequest: fetchUpdate@{ENTITYNAME},
    deleteRequest: fetchDelete@{ENTITYNAME},
    entityName: '@{TABLEDESC}',
    resolveRecordLabel: (record) => record?.@{MAJORCOLUMN0} || record?.@{PRIMARYKEYCOLUMN0},
    refreshCreate,
    refreshUpdate,
    refreshRemove
  })
  handleEditAction = (row) => showDialog('edit', row)
  handleDeleteAction = handleDelete
  const buildPreviewDialogMeta = (rows) => {
    const now = new Date()
    return {
      reportTitle,
      reportDate: now.toLocaleDateString('zh-CN'),
      printedAt: now.toLocaleString('zh-CN', { hour12: false }),
      operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
      count: rows.length
    }
  }
  const resolvePrintRecords = async (payload) => {
    const response =
      Array.isArray(payload?.ids) && payload.ids.length > 0
        ? await fetchGet@{ENTITYNAME}Many(payload.ids)
        : await fetch@{ENTITYNAME}Page({
            ...reportQueryParams.value,
            current: 1,
            pageSize:
              Number(pagination.total) > 0
                ? Number(pagination.total)
                : Number(payload?.pageSize) || 20
          })
    return defaultResponseAdapter(response).records
  }
  const {
    previewVisible,
    previewRows,
    previewMeta,
    handlePreviewVisibleChange,
    handleExport,
    handlePrint
  } = usePrintExportPage({
    downloadFileName: '@{KEBABENTITYNAME}.xlsx',
    requestExport: (payload) =>
      fetchExport@{ENTITYNAME}Report(payload, {
        headers: {
          Authorization: userStore.accessToken || ''
        }
      }),
    resolvePrintRecords,
    buildPreviewRows: (records) => build@{ENTITYNAME}PrintRows(records),
    buildPreviewMeta: (rows) => buildPreviewDialogMeta(rows)
  })
  const reportColumns = computed(() => resolve@{ENTITYNAME}ReportColumns(columns.value))
  const resolvedPreviewMeta = computed(() =>
    build@{ENTITYNAME}ReportMeta({
      previewMeta: previewMeta.value,
      count: previewRows.value.length,
      titleAlign: @{ENTITYPREFIX}_REPORT_STYLE.titleAlign,
      titleLevel: @{ENTITYPREFIX}_REPORT_STYLE.titleLevel
    })
  )
  function handleSearch(params) {
    replaceSearchParams(build@{ENTITYNAME}SearchParams(params))
    getData()
  }
  function handleReset() {
    Object.assign(searchForm.value, create@{ENTITYNAME}SearchState())
    resetSearchParams()
  }
</script>
rsf-framework/src/main/resources/templates/rsf-design/PageHelpers.txt
New file
@@ -0,0 +1,245 @@
const FIELD_OPTIONS = @{FIELDOPTIONSCONTENT}
export const @{ENTITYPREFIX}_REPORT_TITLE = '@{TABLEDESC}报表'
export const @{ENTITYPREFIX}_REPORT_STYLE = {
  titleAlign: 'center',
  titleLevel: 'strong',
  orientation: 'portrait',
  density: 'compact',
  showSequence: true
}
function normalizeText(value) {
  return String(value ?? '').trim()
}
function hasValue(value) {
  return value !== '' && value !== null && value !== undefined
}
function toOptionalNumber(value) {
  if (!hasValue(value)) {
    return void 0
  }
  const parsed = Number(value)
  return Number.isNaN(parsed) ? void 0 : parsed
}
function toOptionalBoolean(value) {
  if (!hasValue(value)) {
    return void 0
  }
  if (value === true || value === false) {
    return value
  }
  if (value === 1 || value === '1' || String(value).toLowerCase() === 'true') {
    return true
  }
  if (value === 0 || value === '0' || String(value).toLowerCase() === 'false') {
    return false
  }
  return Boolean(value)
}
function buildNumberField(key, value) {
  return hasValue(value) ? { [key]: Number(value) } : {}
}
function cloneOptions(options = []) {
  return options.map((option) => ({ ...option }))
}
function normalizeStatusValue(status) {
  if (!hasValue(status)) {
    return ''
  }
  return String(status)
}
function formatBooleanText(value) {
  if (value === true) {
    return '是'
  }
  if (value === false) {
    return '否'
  }
  return ''
}
function resolveStatusType(option = {}) {
  const label = String(option.label || '')
  const value = normalizeStatusValue(option.value)
  if (value === '1' || value === 'true' || /正常|启用|有效|开启|是/.test(label)) {
    return 'success'
  }
  if (value === '0' || value === 'false' || /禁用|冻结|停用|关闭|否/.test(label)) {
    return 'danger'
  }
  return 'info'
}
function resolveStatusBool(option = {}) {
  const value = normalizeStatusValue(option.value)
  if (value === '1' || value === 'true') {
    return true
  }
  if (value === '0' || value === 'false') {
    return false
  }
  return false
}
export function create@{ENTITYNAME}SearchState() {
  return {
    condition: '',
@{SEARCHSTATECONTENT}
  }
}
export function create@{ENTITYNAME}FormState() {
  return {
@{FORMSTATECONTENT}
  }
}
export function get@{ENTITYNAME}PaginationKey() {
  return {
    current: 'current',
    size: 'pageSize'
  }
}
export function get@{ENTITYNAME}FieldOptions(field) {
  return cloneOptions(FIELD_OPTIONS[field] || [])
}
export function get@{ENTITYNAME}StatusOptions() {
  return get@{ENTITYNAME}FieldOptions('status')
}
export function get@{ENTITYNAME}StatusMeta(status) {
  const options = FIELD_OPTIONS.status || []
  const normalizedStatus = normalizeStatusValue(status)
  const matchedOption = options.find((option) => normalizeStatusValue(option.value) === normalizedStatus)
  if (!matchedOption) {
    return {
      type: 'info',
      text: normalizeText(status) || '未知',
      bool: false
    }
  }
  return {
    type: resolveStatusType(matchedOption),
    text: matchedOption.label,
    bool: resolveStatusBool(matchedOption)
  }
}
export function build@{ENTITYNAME}SearchParams(params = {}) {
  const searchParams = {
    condition: normalizeText(params.condition),
@{SEARCHPARAMSCONTENT}
  }
  return Object.fromEntries(Object.entries(searchParams).filter(([, value]) => hasValue(value)))
}
export function build@{ENTITYNAME}PageQueryParams(params = {}) {
  return {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20,
    ...build@{ENTITYNAME}SearchParams(params)
  }
}
export const @{ENTITYPREFIX}_REPORT_COLUMNS = [
@{REPORTCOLUMNSCONTENT}
]
const @{ENTITYPREFIX}_REPORT_SOURCE_ALIAS = {
@{REPORTSOURCEALIASCONTENT}
}
export function get@{ENTITYNAME}ReportColumns() {
  return @{ENTITYPREFIX}_REPORT_COLUMNS.map((column) => ({ ...column }))
}
export function resolve@{ENTITYNAME}ReportColumns(columns = []) {
  if (!Array.isArray(columns)) {
    return []
  }
  const allowedColumns = new Map(@{ENTITYPREFIX}_REPORT_COLUMNS.map((column) => [column.source, column]))
  const seenSources = new Set()
  return columns
    .map((column) => {
      if (!column || typeof column !== 'object') {
        return null
      }
      const source =
        @{ENTITYPREFIX}_REPORT_SOURCE_ALIAS[column.source ?? column.prop] ??
        column.source ??
        column.prop
      if (!source || !allowedColumns.has(source) || seenSources.has(source)) {
        return null
      }
      seenSources.add(source)
      const allowedColumn = allowedColumns.get(source)
      return {
        source,
        label: column.label || allowedColumn.label
      }
    })
    .filter(Boolean)
}
export function build@{ENTITYNAME}ReportMeta({
  previewMeta = {},
  count = 0,
  titleAlign = @{ENTITYPREFIX}_REPORT_STYLE.titleAlign,
  titleLevel = @{ENTITYPREFIX}_REPORT_STYLE.titleLevel
} = {}) {
  return {
    reportTitle: @{ENTITYPREFIX}_REPORT_TITLE,
    reportDate: previewMeta.reportDate,
    printedAt: previewMeta.printedAt,
    operator: previewMeta.operator,
    count,
    reportStyle: {
      ...@{ENTITYPREFIX}_REPORT_STYLE,
      titleAlign,
      titleLevel
    }
  }
}
export function build@{ENTITYNAME}DialogModel(record = {}) {
  return {
    ...create@{ENTITYNAME}FormState(),
@{DIALOGMODELCONTENT}
  }
}
export function build@{ENTITYNAME}SavePayload(formData = {}) {
  return {
@{SAVEPAYLOADCONTENT}
  }
}
export function normalize@{ENTITYNAME}ListRow(record = {}) {
  const statusMeta = get@{ENTITYNAME}StatusMeta(record.statusBool ?? record.status)
  return {
    ...record,
@{NORMALIZEROWCONTENT}
  }
}
export function build@{ENTITYNAME}PrintRows(records = []) {
  if (!Array.isArray(records)) {
    return []
  }
  return records.map((record) => normalize@{ENTITYNAME}ListRow(record))
}
rsf-framework/src/main/resources/templates/rsf-design/Search.txt
New file
@@ -0,0 +1,66 @@
<template>
  <ArtSearchBar
    ref="searchBarRef"
    v-model="formData"
    :items="formItems"
    :showExpand="false"
    @reset="handleReset"
    @search="handleSearch"
  />
</template>
<script setup>
  import { create@{ENTITYNAME}SearchState, get@{ENTITYNAME}FieldOptions } from '../@{SIMPLEENTITYNAME}Page.helpers'
  const props = defineProps({
    modelValue: { required: true }
  })
  const emit = defineEmits(['update:modelValue', 'search', 'reset'])
  const searchBarRef = ref()
  const formData = computed({
    get: () => props.modelValue,
    set: (val) => emit('update:modelValue', val)
  })
  function createInputSearchItem(label, key, placeholder) {
    return {
      label,
      key,
      type: 'input',
      props: {
        placeholder,
        clearable: true
      }
    }
  }
  function createSelectSearchItem(label, key, placeholder, options) {
    return {
      label,
      key,
      type: 'select',
      props: {
        placeholder,
        clearable: true,
        options
      }
    }
  }
  const formItems = computed(() => [
    createInputSearchItem('关键字', 'condition', '请输入@{TABLEDESC}关键字'),
@{SEARCHITEMSCONTENT}
  ])
  function handleReset() {
    emit('update:modelValue', create@{ENTITYNAME}SearchState())
    emit('reset')
  }
  async function handleSearch(params) {
    await searchBarRef.value.validate()
    emit('search', params)
  }
</script>
rsf-framework/src/main/resources/templates/rsf-design/TableColumns.txt
New file
@@ -0,0 +1,96 @@
import { h } from 'vue'
import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
import { ElTag } from 'element-plus'
import { get@{ENTITYNAME}StatusMeta } from './@{SIMPLEENTITYNAME}Page.helpers'
const MORE_ACTIONS = [
  {
    key: 'edit',
    label: '编辑',
    icon: 'ri:edit-2-line',
    auth: 'edit'
  },
  {
    key: 'delete',
    label: '删除',
    icon: 'ri:delete-bin-4-line',
    color: '#f56c6c',
    auth: 'delete'
  }
]
function resolveCellValue(row, prop) {
  const value = row?.[prop]
  return value === '' || value === void 0 || value === null ? '-' : value
}
function createTextColumn(prop, label, minWidth, extra = {}) {
  return {
    prop,
    label,
    minWidth,
    showOverflowTooltip: true,
    formatter: (row) => resolveCellValue(row, prop),
    ...extra
  }
}
function createNumberColumn(prop, label, width, extra = {}) {
  return {
    prop,
    label,
    width,
    formatter: (row) => resolveCellValue(row, prop),
    ...extra
  }
}
function createTagColumn(prop, label, width, resolveMeta) {
  return {
    prop,
    label,
    width,
    formatter: (row) => {
      const statusMeta = resolveMeta(row)
      return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
    }
  }
}
function buildMoreActions(handleEdit, handleDelete) {
  return MORE_ACTIONS.filter((item) => {
    if (item.key === 'edit') {
      return typeof handleEdit === 'function'
    }
    if (item.key === 'delete') {
      return typeof handleDelete === 'function'
    }
    return true
  })
}
export function create@{ENTITYNAME}TableColumns({ handleEdit, handleDelete } = {}) {
  return [
    { type: 'selection', width: 52, fixed: 'left' },
@{TABLECOLUMNSCONTENT}
    {
      prop: 'operation',
      label: '操作',
      width: 120,
      align: 'center',
      fixed: 'right',
      formatter: (row) =>
        h(ArtButtonMore, {
          list: buildMoreActions(handleEdit, handleDelete),
          onClick: (item) => {
            if (item.key === 'edit') {
              handleEdit?.(row)
            }
            if (item.key === 'delete') {
              handleDelete?.(row)
            }
          }
        })
    }
  ]
}
rsf-server/src/main/java/com/vincent/rsf/server/common/CodeBuilder.java
@@ -2,6 +2,7 @@
import com.vincent.rsf.framework.generators.ReactGenerator;
import com.vincent.rsf.framework.generators.RsfDesignGenerator;
import com.vincent.rsf.framework.generators.constant.SqlOsType;
/**
@@ -10,6 +11,10 @@
public class CodeBuilder {
    public static void main(String[] args) throws Exception {
        buildRsfDesign();
    }
    public static void buildReactAdmin() throws Exception {
        ReactGenerator generator = new ReactGenerator();
        generator.backendPrefixPath = "rsf-server/";
        generator.frontendPrefixPath = "rsf-admin/";
@@ -25,7 +30,27 @@
        generator.table = "man_matnr_restriction_warehouse";
        generator.tableDesc = "物料限制";
        generator.packagePath = "com.vincent.rsf.server.manager";
        generator.build();
    }
    public static void buildRsfDesign() throws Exception {
        RsfDesignGenerator generator = new RsfDesignGenerator();
        generator.backendPrefixPath = "rsf-server/";
        generator.frontendPrefixPath = "rsf-design/";
        generator.sqlOsType = SqlOsType.MYSQL;
        generator.url = "127.0.0.1:3306/rsf";
        generator.username = "root";
        generator.password = "root";
//        generator.url="47.97.1.152:51433;databasename=jkasrs";
//        generator.username="sa";
//        generator.password="Zoneyung@zy56$";
        generator.table = "man_matnr_restriction_warehouse";
        generator.tableDesc = "物料限制";
        generator.packagePath = "com.vincent.rsf.server.manager";
        generator.frontendViewPath = "manager/matnr-restriction-warehouse";
        generator.frontendApiModule = "matnr-restriction-warehouse";
        generator.build();
    }
/*