From e12fb4e6e8e0a408e81ce05a269a15cc535d8c78 Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期三, 01 四月 2026 16:27:17 +0800
Subject: [PATCH] #生成器

---
 rsf-framework/src/main/resources/templates/rsf-design/EditDialog.txt                     |  136 ++++
 rsf-framework/src/main/resources/templates/rsf-design/TableColumns.txt                   |   96 ++
 rsf-framework/src/main/resources/templates/rsf-design/Index.txt                          |  240 +++++++
 rsf-framework/src/main/resources/templates/rsf-design/Controller.txt                     |  140 ++++
 rsf-framework/src/main/resources/templates/rsf-design/PageHelpers.txt                    |  245 +++++++
 rsf-framework/src/main/java/com/vincent/rsf/framework/generators/RsfDesignGenerator.java |  946 ++++++++++++++++++++++++++++
 rsf-server/src/main/java/com/vincent/rsf/server/common/CodeBuilder.java                  |   25 
 rsf-framework/src/main/resources/templates/rsf-design/Api.txt                            |   84 ++
 rsf-framework/src/main/resources/templates/rsf-design/Search.txt                         |   66 ++
 9 files changed, 1,978 insertions(+), 0 deletions(-)

diff --git a/rsf-framework/src/main/java/com/vincent/rsf/framework/generators/RsfDesignGenerator.java b/rsf-framework/src/main/java/com/vincent/rsf/framework/generators/RsfDesignGenerator.java
new file mode 100644
index 0000000..c805452
--- /dev/null
+++ b/rsf-framework/src/main/java/com/vincent/rsf/framework/generators/RsfDesignGenerator.java
@@ -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("璇疯緭鍏able锛�");
+        }
+        if (Cools.isEmpty(this.tableDesc)) {
+            throw new RuntimeException("璇疯緭鍏ableDesc锛�");
+        }
+        if (Cools.isEmpty(this.packagePath)) {
+            throw new RuntimeException("璇疯緭鍏ackagePath锛�");
+        }
+        if (frontend) {
+            if (Cools.isEmpty(frontendPrefixPath)) {
+                throw new RuntimeException("璇疯緭鍏rontendPrefixPath锛�");
+            }
+            if (Cools.isEmpty(frontendViewPath)) {
+                throw new RuntimeException("璇疯緭鍏rontendViewPath锛�");
+            }
+            if (Cools.isEmpty(frontendApiModule)) {
+                throw new RuntimeException("璇疯緭鍏rontendApiModule锛�");
+            }
+        }
+    }
+
+    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;
+    }
+}
diff --git a/rsf-framework/src/main/resources/templates/rsf-design/Api.txt b/rsf-framework/src/main/resources/templates/rsf-design/Api.txt
new file mode 100644
index 0000000..7dfb175
--- /dev/null
+++ b/rsf-framework/src/main/resources/templates/rsf-design/Api.txt
@@ -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)
+  })
+}
diff --git a/rsf-framework/src/main/resources/templates/rsf-design/Controller.txt b/rsf-framework/src/main/resources/templates/rsf-design/Controller.txt
new file mode 100644
index 0000000..24cad1e
--- /dev/null
+++ b/rsf-framework/src/main/resources/templates/rsf-design/Controller.txt
@@ -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);
+    }
+}
diff --git a/rsf-framework/src/main/resources/templates/rsf-design/EditDialog.txt b/rsf-framework/src/main/resources/templates/rsf-design/EditDialog.txt
new file mode 100644
index 0000000..393e645
--- /dev/null
+++ b/rsf-framework/src/main/resources/templates/rsf-design/EditDialog.txt
@@ -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>
diff --git a/rsf-framework/src/main/resources/templates/rsf-design/Index.txt b/rsf-framework/src/main/resources/templates/rsf-design/Index.txt
new file mode 100644
index 0000000..539cdb7
--- /dev/null
+++ b/rsf-framework/src/main/resources/templates/rsf-design/Index.txt
@@ -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>
diff --git a/rsf-framework/src/main/resources/templates/rsf-design/PageHelpers.txt b/rsf-framework/src/main/resources/templates/rsf-design/PageHelpers.txt
new file mode 100644
index 0000000..4bb0d1e
--- /dev/null
+++ b/rsf-framework/src/main/resources/templates/rsf-design/PageHelpers.txt
@@ -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' || /姝e父|鍚敤|鏈夋晥|寮�鍚瘄鏄�/.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))
+}
diff --git a/rsf-framework/src/main/resources/templates/rsf-design/Search.txt b/rsf-framework/src/main/resources/templates/rsf-design/Search.txt
new file mode 100644
index 0000000..13daed5
--- /dev/null
+++ b/rsf-framework/src/main/resources/templates/rsf-design/Search.txt
@@ -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>
diff --git a/rsf-framework/src/main/resources/templates/rsf-design/TableColumns.txt b/rsf-framework/src/main/resources/templates/rsf-design/TableColumns.txt
new file mode 100644
index 0000000..187ed92
--- /dev/null
+++ b/rsf-framework/src/main/resources/templates/rsf-design/TableColumns.txt
@@ -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)
+            }
+          }
+        })
+    }
+  ]
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/common/CodeBuilder.java b/rsf-server/src/main/java/com/vincent/rsf/server/common/CodeBuilder.java
index 1b5dba8..830935a 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/common/CodeBuilder.java
+++ b/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();
     }
 /*

--
Gitblit v1.9.1